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.

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.

Try with my idea → Sign in