I Built a Real-Time Stock Price Tracker with Django, Redis and WebSockets

Published: (February 14, 2026 at 05:46 PM EST)
4 min read
Source: Dev.to

Source: Dev.to

Ebenezer Lamptey

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

#RoleDescription
1Celery brokerPasses tasks between Celery Beat and the worker.
2Price cacheStores the last 5 prices per stock as a Redis List.
3Channel Layer backendLets 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
0 views
Back to Blog

Related posts

Read more »