Alphanume

Insights

Build a Premarket Movers Research Watchlist in Python

Alphanume Team · October 27, 2026

A useful premarket watchlist needs a dated input, explicit filters, and a record of when you obtained it. This Python workflow retrieves Alphanume's ranked microcap drop-risk candidates, saves every API page, and exports a CSV containing only the fields needed for morning research.

The source is the Pre-Market Drop Risk dataset, not a market-wide live movers feed. It contains selected US microcaps ranked by modeled probability of an unadjusted regular-session open-to-close decline of at least 5%. The feature cutoff is 09:00 ET; that is distinct from the time a response reaches your computer.

Choose the list you intend to review

Start with a signal date and one clear selection rule. The example asks for the top ten returned ranks on that date. Ten is a demonstration choice, not an optimized threshold. Requesting a fixed date avoids quietly mixing yesterday's candidates into today's review when a fresh list has not appeared.

Set SIGNAL_DATE to a date in YYYY-MM-DD format and set ALPHANUME_API_KEY to your own Pro key in the environment. Install requests in the Python environment you use to run the script. This endpoint requires Pro access for both current rows and history; there is no public row preview.

You can add min_price=1 to review only candidates priced at least $1 at the cutoff, or sub_dollar=1 to investigate the sub-$1 cohort. That changes the returned selection, not the model's original daily ranks. A rank gap after filtering does not mean the API lost a row.

Retrieve every page and keep the receipts

The API contract orders results by descending signal date and ascending daily rank. When more pages exist, next_cursor.date becomes cursor_date and next_cursor.rank_for_date becomes cursor_rank. Those parameter names differ from the response keys, so do not copy the cursor into the request unchanged.

import csv
import json
import os
from datetime import date, datetime, timezone
from pathlib import Path
import requests

URL = "https://api.alphanume.com/v1/premarket-drop-risk"
KEY = os.environ["ALPHANUME_API_KEY"]
SIGNAL_DATE = date.fromisoformat(os.environ["SIGNAL_DATE"]).isoformat()
OUT = Path("premarket-watchlist") / (
    SIGNAL_DATE + "-" + datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
)
OUT.mkdir(parents=True, exist_ok=False)
params = {"date": SIGNAL_DATE, "max_rank": 10, "api_key": KEY}
fields = [
    "date", "ticker", "rank_for_date", "prob_drop", "px_at_trading",
    "sub_dollar", "market_cap", "premarket_return_at_cutoff",
] + [f"t_{i}" for i in range(10)]
rows, seen_cursors = [], set()
receipts = []
page_no = 0

with requests.Session() as session:
    while True:
        page_no += 1
        try:
            response = session.get(URL, params=params, timeout=(10, 60))
        except requests.RequestException:
            raise SystemExit("Request failed; no watchlist was exported.") from None
        if not response.ok:
            # Avoid printing the authenticated request URL.
            raise SystemExit(f"API returned HTTP {response.status_code}.")
        try:
            page = response.json()
        except ValueError:
            raise SystemExit("API did not return valid JSON.") from None
        if not isinstance(page, dict) or not isinstance(page.get("data"), list):
            raise SystemExit("Unexpected response shape.")
        if not isinstance(page.get("has_more"), bool):
            raise SystemExit("Missing pagination flag.")
        if not all(isinstance(row, dict) for row in page["data"]):
            raise SystemExit("Unexpected row shape.")
        retrieved_at = datetime.now(timezone.utc).isoformat()
        name = f"page-{page_no:03d}.json"
        (OUT / name).write_text(json.dumps(page, indent=2), encoding="utf-8")
        receipts.append({"file": name, "retrieved_at_utc": retrieved_at})
        rows.extend(page["data"])
        if not page["has_more"]:
            break
        cursor = page.get("next_cursor")
        if not isinstance(cursor, dict):
            raise SystemExit("Response says more pages exist without a cursor.")
        pair = (cursor.get("date"), cursor.get("rank_for_date"))
        if pair[0] is None or pair[1] is None or pair in seen_cursors:
            raise SystemExit("Missing or repeated cursor; stopped incomplete pull.")
        seen_cursors.add(pair)
        params.update(cursor_date=pair[0], cursor_rank=pair[1])

keys = [(row["date"], row["ticker"]) for row in rows]
if len(keys) != len(set(keys)):
    raise SystemExit("Duplicate date/ticker rows; inspect the saved pages.")
rows.sort(key=lambda row: (row["rank_for_date"], row["ticker"]))
with (OUT / "watchlist.csv").open("w", newline="", encoding="utf-8") as file:
    writer = csv.DictWriter(file, fieldnames=fields)
    writer.writeheader()
    writer.writerows({name: row.get(name) for name in fields} for row in rows)
manifest = {
    "endpoint": URL,
    "filters": {k: v for k, v in params.items()
                if k not in {"api_key", "cursor_date", "cursor_rank"}},
    "pages": receipts,
    "rows": len(rows),
    "purpose": "research watchlist; outcome fields excluded from CSV",
}
(OUT / "manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
print(f"Saved {len(rows)} candidates to {OUT / 'watchlist.csv'}")

The script creates a fresh output directory for each run. It saves the complete JSON pages before producing the narrower CSV, so the research input remains auditable. Raw historical pages can include later outcomes. Keep those archives separate from the decision-facing export: the CSV deliberately omits intraday_return_pct and all return_lead_* fields.

There is no automatic fallback to a previous date and no exported partial list after a failed page. HTTP errors, an invalid response shape, a missing cursor, or a repeated cursor stop the run. This makes a failed request visible instead of turning it into an apparently clean but incomplete screen.

Read the exported fields without changing their meaning

FieldResearch useInterpretation to preserve
prob_dropPrioritize downside review0.70 is a modeled 70% chance of the defined event, not a 70% expected fall.
rank_for_dateOrder that day's returned candidatesRank is relative to the dataset's selected universe, not all market movers.
px_at_trading and sub_dollarSeparate price cohortsCutoff observations, not live quotes or opening fills.
market_capCompany-size contextPoint-in-time size attached to the signal date; missing means unavailable.
t_0 through t_9Inspect the premarket pathCumulative clock-mark returns; zero can mean no prior print.

The return path runs from 04:30 through 09:00 ET in half-hour marks. premarket_return_at_cutoff equals t_9. Do not add the ten marks together or treat them as ten independent period returns.

The exported watchlist does not contain premarket volume or bid-ask spreads because the source does not serve those fields. For a volume or liquidity rule, join an appropriate timestamped market-data source and record its provenance. Deriving a “liquidity score” from the presence of a price mark alone would give the field more meaning than its contract supports.

Make the receipt time part of the morning decision

The manifest timestamps when each response was obtained in UTC. That is your own observation record, not a server publication timestamp, and it cannot prove that a row was obtainable earlier. If the run finishes after your intended entry time, do not use it as evidence for a pre-entry decision.

A successful zero-row response writes a header-only CSV and a manifest with zero candidates. It is different from an HTTP failure. It also does not independently establish why the list is empty: there may be no selected candidates, the date may be outside coverage, or current rows may not yet be present. Check the date and operational context before drawing a conclusion.

Running this script today for a historical date creates a historical research extract. It does not recreate a file you actually received on that morning. For a forward observation log, run the same saved rules on successive trading mornings and retain each receipt directory.

Add company context and locate observations as separate joins

Attach source-document notes after the pull: the catalyst URL and timestamp, relevant dilution filings, and any shelf capacity you need to investigate. Keep later lifecycle outcomes out of the morning interpretation. A future offering is not information that existed merely because it now appears in a filing history.

For short research, the Short Locates dataset can add quoted costs, offered status, and quantity. Use the actual snapshot_time_utc and only observations already available at your decision time. Its history starts October 8, 2026; it does not supply borrow information for earlier watchlists. A quote does not reserve shares or confirm execution through your own broker.

This tutorial stops at a research queue. It neither submits orders nor measures a strategy's profitability. The next step is to use the premarket evaluation checklist to document each candidate's review. When you are ready to evaluate a historical rule, move to the separate backtest workflow, where outcomes belong in an explicitly later grading stage.