Support

Build a real-time stock screener

In this example, we will build a real-time scanner for approximately 9,000 tickers in the U.S. equity markets.

We'll use the OHLCV schema from the Databento US Equities Summary (EQUS.SUMMARY) dataset. This dataset provides end-of-day summary statistics for all RegNMS symbols via the Nasdaq NLS+ feed. These statistics include consolidated OHLCV data across all U.S. equity venues.

We'll also use the MBP-1 schema from the Databento US Equities Mini (EQUS.MINI) dataset. This is a derived top-of-book dataset that provides aggregated BBO and trade information from a variety of proprietary market data feeds.

Info
Info

A Databento US Equities Standard subscription is required to run this example.

import databento as db
import pandas as pd

class PriceMovementScanner:
    """Scanner for detecting large price movements in all US equities."""

    def __init__(
        self,
        pct_threshold: float = 0.03,  # Default threshold for alert
    ) -> None:
        """Initialize scanner with a configurable threshold."""
        self.pct_threshold = pct_threshold
        self.start = pd.Timestamp.now(tz="US/Eastern").normalize()
        self.premarket_start = self.start.replace(hour=4)  # Start of today's pre-market session

        self.symbol_directory: dict[int, str] = {}
        self.last_day_lookup: dict[str, float] = self.get_last_day_lookup()
        self.is_signal_lit: dict[str, bool] = {symbol: False for symbol in self.last_day_lookup}

    def get_last_day_lookup(self) -> dict[str, float]:
        """Get yesterday's closing prices for all symbols."""
        client = db.Historical()

        # Get OHLCV-1d data from the previous session
        data = client.timeseries.get_range(
            dataset="EQUS.SUMMARY",
            schema="ohlcv-1d",
            symbols="ALL_SYMBOLS",
            start=(self.start - pd.offsets.BusinessDay(1)).date(),
        )

        # Request symbology: This is required for ALL_SYMBOLS requests
        # which don't automatically map instrument ID to raw ticker symbol
        symbology_json = data.request_symbology(client)
        data.insert_symbology_json(symbology_json)

        df = data.to_df()

        return df.set_index("symbol")["close"].to_dict()

    def scan(self, event: db.DBNRecord) -> None:
        """
        Scan for large price movements in market data events.
        """
        if isinstance(event, db.SymbolMappingMsg):
            self.symbol_directory[event.instrument_id] = event.stype_out_symbol
            return

        if not isinstance(event, db.MBP1Msg):
            return

        symbol = self.symbol_directory[event.instrument_id]

        # Skip if alert already triggered for the symbol
        if self.is_signal_lit[symbol]:
            return

        bid = event.levels[0].pretty_bid_px
        ask = event.levels[0].pretty_ask_px

        # Skip if one side of the book is empty
        if pd.isna(bid) or pd.isna(ask):
            return

        # Calculate change since the previous close
        mid = (bid + ask) / 2

        previous_close = self.last_day_lookup.get(symbol)
        # New listing, no data from previous session
        if previous_close is None:
            return

        pct_change = (mid - previous_close) / previous_close

        # Trigger alert if the threshold is exceeded and no previous alert
        if abs(pct_change) > self.pct_threshold:
            ts = event.pretty_ts_event.tz_convert("US/Eastern")
            print(
                f"[{ts.isoformat()}] {symbol} moved by {pct_change:.2%} "
                f"(current: {mid:.4f}, previous: {previous_close:.4f})",
            )
            self.is_signal_lit[symbol] = True


if __name__ == "__main__":
    # Instantiate scanner class
    scanner = PriceMovementScanner()

    # Create a live client
    live = db.Live(key="$YOUR_API_KEY")

    # Subscribe MBP-1 schema for ALL_SYMBOLS
    # Start subscription at pre-market open
    live.subscribe(
        dataset="EQUS.MINI",
        schema="mbp-1",
        symbols="ALL_SYMBOLS",
        start=scanner.premarket_start,
    )

    # Add callback and start
    live.add_callback(scanner.scan)
    live.start()

    # Run indefinitely
    live.block_for_close()
[2025-05-06T04:00:00.023079908-04:00] CORN moved by 4.19% (current: 19.1400, previous: 18.3700)
[2025-05-06T04:00:00.046518714-04:00] CPER moved by 4.04% (current: 30.4200, previous: 29.2400)
[2025-05-06T04:00:00.047119403-04:00] FETH moved by -8.56% (current: 16.6050, previous: 18.1600)
[2025-05-06T04:00:00.069275113-04:00] XXRP moved by -3.80% (current: 29.3300, previous: 30.4900)
[2025-05-06T04:00:00.118536539-04:00] ETHT moved by 24.40% (current: 5.7100, previous: 4.5900)
...
[2025-05-06T09:33:43.338614253-04:00] XMVM moved by 19.04% (current: 62.5450, previous: 52.5400)
[2025-05-06T09:33:46.814411294-04:00] BELFB moved by -5.09% (current: 65.4500, previous: 68.9600)
[2025-05-06T09:33:53.530064671-04:00] CATO moved by -3.25% (current: 2.2350, previous: 2.3100)
[2025-05-06T09:33:53.711869173-04:00] WRB moved by -11.42% (current: 64.2150, previous: 72.4900)
[2025-05-06T09:33:55.223052174-04:00] QUS moved by -3.72% (current: 149.8250, previous: 155.6100)