Home · Blog
Tutorial

Build a daily Alpaca trading bot on top of the Stock Screener API

June 3, 2026 · ~6 min read

Last time I wired screener picks into an eToro bot by hand-rolling REST calls. This time the broker does most of the work for us. Alpaca has a clean official Python SDK, a real paper-trading mode, and notional (dollar-amount) orders that handle fractional shares, so the same daily-rebalance idea fits in well under 100 lines.

The complete code is in the alpaca-python-trading-bot repo. This post walks through how it works. Everything runs against an Alpaca paper-trading account, which is where you should keep it until you trust it.

Read this first. Educational tutorial, not investment advice. Keep paper=True. An automated bot can churn through orders fast, so read every line and watch it run before you ever consider real money.

The idea in one sentence

Each trading day, ask the Stock Screener API which tickers a screener is holding, then make the Alpaca account hold exactly that set, equal-weighted: sell what dropped out, buy what's new.

What you need

Python 3.10+ and two libraries. Alpaca ships an official SDK, so there's no REST boilerplate for the trading side.

pip install alpaca-py requests

Four environment variables: two for the screener, two for your Alpaca paper account (generate the keys from the Alpaca dashboard under Paper Trading → API Keys).

RAPIDAPI_KEY=your-rapidapi-key
RAPIDAPI_HOST=stock-screener6.p.rapidapi.com
ALPACA_API_KEY_ID=your-alpaca-key-id
ALPACA_API_SECRET_KEY=your-alpaca-secret

Setting up the clients

The screener is a plain HTTP call with two RapidAPI headers. The Alpaca side is a single TradingClient with paper=True, which is the only switch separating paper from live.

import os
import requests
from alpaca.trading.client import TradingClient
from alpaca.trading.enums import OrderSide, TimeInForce
from alpaca.trading.requests import MarketOrderRequest

RAPIDAPI_HOST = os.environ["RAPIDAPI_HOST"]
SCREENER_HEADERS = {
    "X-RapidAPI-Key":  os.environ["RAPIDAPI_KEY"],
    "X-RapidAPI-Host": RAPIDAPI_HOST,
}

trading_client = TradingClient(
    api_key=os.environ["ALPACA_API_KEY_ID"],
    secret_key=os.environ["ALPACA_API_SECRET_KEY"],
    paper=True,
)

Step 1: pull the picks

One request to /tickers/latest returns the current tickers for a screener.

def fetch_picks(screener_id: str) -> list[str]:
    """Return today's tickers for a given screener."""
    url = f"https://{RAPIDAPI_HOST}/tickers/latest"
    resp = requests.get(
        url,
        headers=SCREENER_HEADERS,
        params={"screener_id": screener_id},
        timeout=15,
    )
    resp.raise_for_status()
    return [row["ticker"] for row in resp.json()]

Step 2: let performance pick the screener

Rather than hard-coding one strategy, the bot can ask the API which screener has the best average return over a window and trade that one. The /stock-screeners/performance endpoint returns screeners ranked by forward return, so we just take the top one with data.

def best_screener(window: str = "1m") -> str:
    """Return the screener_id with the best avg_return_pct over `window`."""
    url = f"https://{RAPIDAPI_HOST}/stock-screeners/performance"
    resp = requests.get(
        url, headers=SCREENER_HEADERS, params={"window": window}, timeout=15,
    )
    resp.raise_for_status()
    screeners = resp.json()["screeners"]
    top = next(s for s in screeners if s.get("avg_return_pct") is not None)
    print(f"Best {window} screener: {top['short_name']} "
          f"({top['avg_return_pct']:.2f}%)")
    return top["screener_id"]

Convenient, but it chases whatever is hot right now. Keep the "one month proves nothing" caution from the performance post in mind before you let a single window steer real allocation.

Step 3: rebalance to equal weight

Here's the core. Pull today's picks, read the account's portfolio value, and split it evenly across the picks. Then reconcile against current holdings: close anything no longer picked, and open the new names. Because Alpaca supports notional orders, we can buy an exact dollar amount per name and let it fill fractional shares, so the equal-weighting is clean even on expensive tickers.

def rebalance(screener_id: str) -> None:
    """Hold an equal-weight basket of today's picks for `screener_id`."""
    picks = set(fetch_picks(screener_id))
    if not picks:
        print("No picks today, skipping.")
        return

    account = trading_client.get_account()
    target_per_position = float(account.portfolio_value) / len(picks)

    current = {p.symbol: p for p in trading_client.get_all_positions()}

    # Sell positions that fell out of the screener
    for symbol in current:
        if symbol not in picks:
            print(f"SELL {symbol} (no longer in screener)")
            trading_client.close_position(symbol)

    # Buy new picks with notional (fractional) orders
    for symbol in picks - set(current):
        print(f"BUY ${target_per_position:,.0f} of {symbol}")
        trading_client.submit_order(
            order_data=MarketOrderRequest(
                symbol=symbol,
                notional=round(target_per_position, 2),
                side=OrderSide.BUY,
                time_in_force=TimeInForce.DAY,
            )
        )

    print(f"Rebalance complete. Holding {len(picks)} positions.")

Notice the bot only adjusts the edges of the basket: it leaves a held name alone if it's still in the picks, and just opens or closes the difference. That keeps order volume (and slippage) down versus selling everything and rebuilding each day.

The entry point and a kill switch

Wiring it together, with one environment flag that lets you stop all trading instantly without touching code:

if __name__ == "__main__":
    if os.environ.get("BOT_DISABLED"):
        print("BOT_DISABLED is set, exiting.")
        raise SystemExit(0)
    rebalance(best_screener("1m"))

Running it every morning

The script holds no state between runs, so any scheduler works. Same three options as before, in order of setup effort:

Local cron

# 9:35 AM ET, weekdays, just after the open
35 9 * * 1-5  cd /path/to/bot && /usr/bin/python3 bot.py >> bot.log 2>&1

GitHub Actions

Store the four credentials as repository secrets and run the bot from a scheduled workflow. No server to maintain, and you get run logs for free.

AWS Lambda + EventBridge

Package the script as a Lambda and fire it on an EventBridge schedule. Worth it if your stack already lives on AWS.

Before you switch off paper

Flipping paper=True to False (and swapping in live keys) is the entire change to go real, which is exactly why it deserves guardrails first:

The takeaway. The screener generates the signal; the bot is just plumbing that keeps the account matched to it. With Alpaca's SDK and notional orders, that plumbing is short and readable, which is exactly what you want in code that places orders. Prove it in paper first, confirm the screener actually earns its keep, and only then think about live capital.

Want the picks behind the bot?

Daily screener picks, consensus signals, and cohort-based performance, all as a simple JSON API, with a free tier.

Get it on RapidAPI →