Discovery: merge %-gainer movers (the wildcards volume-ranking can't see)

Forensic from the source trader's 2026-07-02 recap: his ENTIRE day (+$8.9k) was CX — a thin
reverse-split name that squeezed $3->$6 on a headline (his "wildcard effect": on dead pre-holiday
days the one mover gets everyone's attention). Our system NEVER SAW IT: zero bars, zero discovery
events. Root cause: discovery ranks by ABSOLUTE SHARE VOLUME (most-actives), and a thin %-squeezer
never cracks a volume-ranked top-N. Every other name he called out we saw and judged the same way
he did (DSY passed/backed-out, CWD too cheap, AHMA rejected) — the gap was pure discovery scope.

- alpaca_paper/client.py: get_movers(top) — /v1beta1/screener/stocks/movers top %-gainers
  (verified working with our keys: CLRO +114%, CWD +73% live).
- discover_active_symbols: wildcard gainers are fetched FIRST (warrants/units filtered:
  '.'-suffixed + 5-char W-endings), deduped with most-actives, same price-band filter.
  Additive + fail-open: a movers failure never breaks the existing actives path.

Discovery/ingestion scope ONLY — every trading gate (confirmation window, quality, VWAP, cap)
still applies unchanged. The immediate value is that shadow tracks + the labeler finally get
BARS for wildcard movers, so the next CX is measurable instead of invisible.

Verified live: universe now leads with gainer-sourced names (CLRO, CWD, USDE, YRD, DSY). 16 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Opus 4.8 committed 2026-07-02 13:02:50 -04:00
1 parent 6c36d04f7a
commit 4fc02a8c71
3 files changed
+185 -1

No files matched your search

+13
View File
@@ -361,6 +361,19 @@ class AlpacaPaperClient:
)
return payload.get("most_actives") or []
def get_movers(self, top: int = 15) -> list[dict]:
"""Top %-change gainers (the 'wildcard' movers). Ranked by PERCENT change, not
share volume — a thin reverse-split name squeezing on a headline (CX on
2026-07-02: $3->$6, the source trader's whole day, invisible to
most-actives-by-volume) shows up here and nowhere else.
Items: {symbol, percent_change, change, price}."""
payload = self._data(
"GET",
"/v1beta1/screener/stocks/movers",
params={"top": top},
)
return payload.get("gainers") or []
def get_news(
self,
symbols: list[str] | None = None,