I Built a Real-Time Stock Price Tracker with Django, Redis and WebSockets
Source: Dev.to
I wanted to have a niche in backend engineering and was drawn to real‑time systems. I wanted to understand how real‑time systems actually work under the hood—not just use them, but build one myself. So I built a stock‑price tracker that:
- fetches live prices every 60 seconds,
- calculates SMAs,
- detects crossover alerts, and
- pushes everything to connected clients over WebSocket.
Below is what I learned.
What it does (every 60 seconds)
- Fetches live prices for 15 stocks from the Finnhub API.
- Saves them to PostgreSQL.
- Caches the last 5 prices per stock in Redis.
- Calculates a 5‑period SMA from the cache.
- Detects bullish/bearish crossover alerts.
- Broadcasts everything to connected WebSocket clients in a single message.
The stack
- Django + DRF – API layer.
- Celery + Celery Beat – task scheduling.
- Redis – caching and Channels backend.
- Django Channels – WebSocket support.
- Uvicorn – ASGI server.
- Finnhub API – market data.
- SQLite – used for the demo DB (I had issues with PostgreSQL on my Mac).
The part that clicked for me
Redis can be used for many things. While I’d used it before as a Celery broker, this project showed me its broader capabilities—especially as a rolling‑window data store.
Three jobs for a single Redis instance
| # | Role | Description |
|---|---|---|
| 1 | Celery broker | Passes tasks between Celery Beat and the worker. |
| 2 | Price cache | Stores the last 5 prices per stock as a Redis List. |
| 3 | Channel Layer backend | Lets Celery talk to Django Channels to broadcast WebSocket messages. |
Seeing one service serve three completely different purposes was a light‑bulb moment.
How the caching works
Each stock has a Redis List that holds its last 5 prices. On every update we run two commands:
RPUSH stock:AAPL:prices 255.78 # add new price to the end
LTRIM stock:AAPL:prices -5 -1 # keep only the newest 5 items
The list never grows beyond five items; the oldest price falls off automatically.
I also used Redis pipelines to batch these commands. Instead of 15 round‑trips (one per stock), I queue all commands and execute them in a single round‑trip—turning 60 trips into just 2.
How the SMA and alerts work
Once five prices are cached, the SMA is a simple average:
sma = sum(last_5_prices) / 5
Crossover detection
# Bullish: price was below SMA, now above
if previous_price < previous_sma and current_price > current_sma:
alert = "bullish"
# Bearish: price was above SMA, now below
if previous_price > previous_sma and current_price < current_sma:
alert = "bearish"
We need the previous values to detect a crossing, which is why caching the SMA matters.
How real‑time broadcasting works
The trickiest part was connecting a background Celery task to a WebSocket client. The answer is the Channel Layer:
Celery task finishes processing
↓
Publishes message to Channel Layer (Redis)
↓
Django Channels picks it up
↓
Pushes to all connected WebSocket clients
Redis acts as the bridge between the two processes.
Example WebSocket payload
{
"type": "stock_update",
"timestamp": "2026-02-14T21:38:22+00:00",
"stocks": [
{ "ticker": "AAPL", "price": 255.78, "sma": 254.32, "alert": null },
{ "ticker": "MSFT", "price": 401.32, "sma": 399.80, "alert": "bullish" },
{ "ticker": "TSLA", "price": 417.44, "sma": 419.10, "alert": "bearish" }
]
}
All 15 stocks are sent in one message, every 60 seconds, automatically.
Running two servers in development
manage.py runserver is a WSGI server, which only handles request/response cycles. WebSockets need a persistent connection, so an ASGI server is required.
# DRF browsable API (WSGI)
python manage.py runserver # → http://localhost:8000
# WebSocket server (ASGI)
uvicorn core.asgi:application --port 8001 # → ws://localhost:8001
REST endpoints live on port 8000, WebSocket connections on port 8001.
What I actually learned
Going in I knew Django and had used Redis a little. Coming out I understand:
- How background task scheduling works with Celery Beat.
- How Redis Lists are perfect for rolling windows of data.
- Why Redis pipelines matter for batching commands.
- The difference between WSGI and ASGI.
- How Django Channels uses a Channel Layer to bridge async and sync code.
- How to structure a real‑time data pipeline end‑to‑end.
Building this made real‑time systems far less mysterious. It’s not magic—it’s just a producer, a channel, and a consumer.
Source code
Future plans
- Simple frontend to show how it works
- Ensure API calls are not made when the market is closed (automatically)
- Migrate database to PostgreSQL
