Build a daily Alpaca trading bot on top of the Stock Screener API
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.
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:
- Position cap. Limit the number of names so a long pick list doesn't shred your capital into tiny slivers.
- Minimum price filter. Skip penny stocks where spreads eat the trade.
- Trade-size limits vs. volume. Don't let a single order be a large fraction of a name's average daily volume.
- Balance checks. Confirm buying power before submitting, so a rebalance never overdraws the account.
- Kill switch. The
BOT_DISABLEDflag above, so you can halt instantly. - Order logging. Record every submitted order for reconciliation.
- Test long in paper. Run it across different market conditions, then start small.
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 →