Insights
How to Automate a Daily Trading Signal in Python (No Framework Required)
Alphanume Team · July 4, 2026
The whole thing is one flat file and four steps: pull, rank, format, send. No queues, no workers, no framework, around twenty lines of Python.
Ask most people why they still check their trading signals by hand and you get the same answer: automating it sounds like a whole engineering project. Queues, workers, a database, a cloud console with forty tabs open. So the signal stays a browser bookmark you open when you remember to. And a signal you have to remember to check is a signal you will forget.
Here is the case against all of that infrastructure: you do not need it. The production skeleton for a daily signal is four steps, pull, rank, format, send, and that order is the entire architecture. The finished file is around twenty lines. Resist the urge to build more, because every moving part you add is a part that can fail silently at 6 AM while you sleep, and a daily signal has exactly one job: arrive. Flat and boring is the professional choice here, not the beginner one.
The example signal
To make the skeleton concrete we need a signal worth waking up to. This walkthrough uses Alphanume's Next-Day Movers dataset: a daily, model-ranked slate of the equities most likely to make a large move next session, scored on implied volatility, realized volatility, and volatility-structure features over a liquid optionable universe. It regenerates every trading day at 3:30 PM ET, every row is point-in-time, and the slate is short, about five names per date.
Across the full history, about 49 percent of flagged names moved more than 5 percent the next session, about 21 percent moved more than 10 percent, and the median absolute move was around 4.8 percent. Note what is missing from those numbers: direction. This is a volatility screen, not a crystal ball. It tells you where the action is likely to be, not which way it breaks. That honesty is exactly what makes it a good candidate for automation: you want the slate in your inbox before the session, moves still pending.
The skeleton is not married to this dataset. Any endpoint that returns JSON rows works identically, which is the point of keeping the file flat: the code barely changes when the question does.
Step 1: Pull
The pull step is a plain requests call against the endpoint:
import requests
import pandas as pd
resp = requests.get(
"https://api.alphanume.com/v1/next-day-movers",
params={"api_key": "alp_your_key"},
)
resp.raise_for_status()
df = pd.json_normalize(resp.json()["data"])The raise_for_status() line matters more in an automated script than anywhere else you will ever use it. When you run code by hand, a bad key or a down endpoint shows up as weird output you notice immediately. When a scheduler runs the code at 6 AM, the failure has no witness. Without that line you get a silent, empty email; with it, the script dies loudly at the exact failing request, which is the difference between debugging in one minute and quietly receiving garbage for a week.
Step 2: Rank
The endpoint returns a window of dates, and you only want the newest slate, sorted so the biggest movers sit at the top of the email:
latest = df["date"].max()
slate = df[df["date"] == latest].sort_values(
"absolute_move", ascending=False
)No hardcoded dates. df["date"].max() means the script pulls whatever is freshest every single day without you touching it. One wrinkle the code depends on: on the newest date, return and absolute_move come back null until that session closes, because the dataset is honest about what was knowable when. A fully pending slate makes the sort a no-op, and that is fine; the freshest slate is the names with their moves still pending, which is exactly the list you want before the open.
Step 3: Format
The email body is a plain-text table. Not HTML, not a chart: a fixed-width block you can read on a phone in three seconds. The only subtlety is a pending marker for the null returns:
rows = [f"Movers for {latest}", ""]
for _, r in slate.iterrows():
move = (
"pending" if pd.isna(r["return"])
else f"{r['return']:+.2f}%"
)
rows.append(f"{r['ticker']:<6} {move}")
msg_body = "\n".join(rows)
print(msg_body)That print() is not a placeholder; it is your test harness. Point the script at older dates from df["date"].unique() and the pending markers turn into realized moves, so you can eyeball the feed for a week before you trust it with your mornings. Verify against history before believing anything forward-looking: the same discipline you would apply to reading a backtest applies to trusting a pipeline.
Step 4: Send
Here is the payoff of keeping the file flat. The only difference between the version that prints and the version that emails is the last step: send msg_body instead of printing it. Python's standard library is enough; no email framework required either:
import smtplib
from email.message import EmailMessage
msg = EmailMessage()
msg["Subject"] = f"Movers for {latest}"
msg["From"] = "you@example.com"
msg["To"] = "you@example.com"
msg.set_content(msg_body)
with smtplib.SMTP_SSL("smtp.example.com", 465) as s:
s.login("you@example.com", "app_password")
s.send_message(msg)Then put the whole file on a schedule: cron on any Linux box, Task Scheduler on Windows, or a scheduled job on a small cloud instance. We cover the scheduling half in detail, including the operational gotchas, in how to schedule a daily data pull in Python; that post and this one are the two halves of the same pipeline.
Why no framework
You will find tutorials that wrap this exact task in a task queue, a message broker, and a container orchestrator. For a daily signal, all of that is surface area for silent failure. The flat file has properties the framework version does not: you can read the whole thing in one screen, you can test it by running it, the failure mode is a loud exception in a log rather than a stuck queue, and moving it to a new machine means copying one file. Complexity is a cost you pay every morning; only take it on when the requirements force it, and a once-a-day pull of a few rows does not.
The skeleton also generalizes without modification. Swap the endpoint for IV rank and the rank step for a filter on rich names, and the same twenty lines deliver a pre-earnings options watchlist. Point it at dilution events and it becomes a daily short-side screen. Pull, rank, format, send is not a script for one signal; it is the shape of every daily signal you will ever run.
Run the skeleton yourself, in the browser
This post condenses a lesson from the automation module of Systematic Trading with Market Data, the interactive course from the quant behind Alphanume Research and The Quant Galore. In Pull, Rank, Format, Send you run the first three steps live in the browser against the real movers feed, and the follow-up lesson bolts on delivery and scheduling. No videos, no setup: the course hands you the data and grades what your code prints.
The first module is free with no account needed; start at the first lesson. And if you want the movers feed itself, explore the dataset or read the API docs; see pricing for access tiers.