ADX in Python — Coding It from Scratch (pandas)

The full +DM/−DM → DI → DX → ADX chain

ADX computed in Python
ADX computed in Python

The ADX is the most involved indicator in this series, but it’s still just a chain of the pieces you’ve already seen: true range, Wilder smoothing, and some ratios. The explainer lays out the logic; here’s the whole chain in pandas.

What you’ll need

  • Python 3.9+, pandas/numpy
  • high, low, close series.

The full chain

import numpy as np
import pandas as pd


def adx(high, low, close, period: int = 14):
    up = high.diff()
    down = -low.diff()

    # Directional movement: only the dominant, positive move counts.
    plus_dm  = up.where((up > down) & (up > 0), 0.0)
    minus_dm = down.where((down > up) & (down > 0), 0.0)

    # True range and its Wilder-smoothed ATR.
    prev_close = close.shift(1)
    tr = pd.concat([high - low, (high - prev_close).abs(),
                    (low - prev_close).abs()], axis=1).max(axis=1)

    alpha = 1 / period
    atr = tr.ewm(alpha=alpha, adjust=False).mean()

    plus_di  = 100 * plus_dm.ewm(alpha=alpha, adjust=False).mean() / atr
    minus_di = 100 * minus_dm.ewm(alpha=alpha, adjust=False).mean() / atr

    dx = 100 * (plus_di - minus_di).abs() / (plus_di + minus_di)
    adx_line = dx.ewm(alpha=alpha, adjust=False).mean()

    return pd.DataFrame({"plus_di": plus_di, "minus_di": minus_di, "adx": adx_line})

Read it top to bottom and it mirrors the explainer exactly: directional movement → smooth and normalize by ATR → DI lines → DX (their normalized separation) → smooth into ADX.

The regime filter, in code

The most valuable use of ADX is as a filter on other signals:

a = adx(df["high"], df["low"], df["close"])
trending = a["adx"] > 25
bull = trending & (a["plus_di"] > a["minus_di"])   # strong up-trend regime

Use bull to gate a trend-following entry, so you only act when a trend actually exists — then backtest whether the filter helps in AlgoGen.

Gotchas

  • The DM rule. Only the larger of the up/down move counts, and only if it’s positive. The .where(...) conditions encode exactly that; getting this wrong is the classic ADX bug.
  • Wilder smoothing & seeding. ewm(alpha=1/period, adjust=False) is Wilder’s smoothing but seeds on the first value; the textbook ADX seeds with sums, so the first ~2×period bars differ slightly before converging — the same seeding nuance as the ATR and EMA.
  • Double lag. ADX is smoothed twice, so it’s slow by construction — treat it as a regime meter, not a trigger.

The lazy one-liner

import pandas_ta as ta
df.ta.adx(length=14, append=True)   # adds ADX_14, DMP_14, DMN_14

Same indicator elsewhere: MQL5, Pine Script, EasyLanguage, NinjaScript.


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. 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