EMA in Python — Coding It from Scratch (pandas)

The recursive formula, the ewm one-liner, and the seeding question

EMA computed in Python
EMA computed in Python

The EMA is a one-line recurrence, as the explainer showed: each bar nudges the previous value toward the new price by a fixed fraction. Here it is in Python, both the explicit loop and the pandas shortcut.

What you’ll need

  • Python 3.9+, pandas/numpy
  • A close price series.

From scratch

import numpy as np


def ema(values, period: int):
    values = np.asarray(values, dtype=float)
    alpha = 2.0 / (period + 1.0)
    out = np.empty_like(values)
    out[0] = values[0]                    # seed with the first price
    for i in range(1, len(values)):
        out[i] = alpha * values[i] + (1 - alpha) * out[i - 1]
    return out

That’s the entire indicator: alpha = 2/(period+1), then each bar is a weighted blend of the new price and the running value. It’s O(1) per bar and needs no history buffer — the efficiency that made the EMA popular in the first place.

The pandas one-liner

df["ema20"] = df["close"].ewm(span=20, adjust=False).mean()

ewm(span=N, adjust=False) is exactly the recurrence above with alpha = 2/(N+1). This is the same call the MACD post uses for its EMAs — no surprise, since the MACD is built from them.

A note on seeding (why versions differ early)

The recurrence needs a starting value, and there are two common choices:

  • Seed with the first price (what the loop above and ewm(adjust=False) do).
  • Seed with an SMA of the first N prices (what some platforms do).

Both converge to the same line, but more slowly than you’d guess: for a 20-period EMA the two seeds still differ by around 1e-4 a hundred bars in, only becoming negligible after a couple hundred bars (the initial gap decays by a factor of 1−α each bar). If your EMA doesn’t match another platform’s early on but agrees deep into the series, this seeding convention is almost always why — not a bug. For long histories it’s irrelevant; for short ones, match your seed to whatever you’re comparing against.

Gotchas

  • adjust=False. With adjust=True (pandas’ default) you get a different, bias-corrected weighting that is not the standard trader’s EMA. Keep it False.
  • EMA ≠ SMA. If you want equal weighting, that’s the SMA. The whole point of the EMA is unequal, recency-biased weighting.

The lazy one-liner

import pandas_ta as ta
df["ema20"] = ta.ema(df["close"], length=20)

Same indicator elsewhere: MQL5, Pine Script, EasyLanguage, NinjaScript. Then test an EMA rule in AlgoGen.


This post is educational, not financial advice. Indicators describe the past; they don’t predict the future. Backtest anything before you risk real money on it.

Sources and further reading

  1. Statistical Forecasting for Inventory Control (Royal Statistical Society and Oxford Academic)
  2. Windowing operations (pandas documentation)

Historical research from the Algogen archive. Not investment advice.

Blog

Have a strategy idea?

Describe it in plain English, preview where your rules fire, then decide whether the evidence is worth a backtest.

Keep me postedHow it works