ADX / DMI (trend strength)
Every indicator so far answers "which direction?" ADX answers a different, arguably more important question: "is there a trend at all, worth trading with a trend-following indicator?" This is the filter chapters 102 and 104 already referenced.
def adx_dmi(df: pd.DataFrame, period: int = 14) -> pd.DataFrame:
high, low, close = df["high"], df["low"], df["close"]
prev_close, prev_high, prev_low = close.shift(1), high.shift(1), low.shift(1)
tr = pd.concat([high - low, (high - prev_close).abs(), (low - prev_close).abs()], axis=1).max(axis=1)
up_move = high - prev_high
down_move = prev_low - low
plus_dm = pd.Series(0.0, index=df.index)
minus_dm = pd.Series(0.0, index=df.index)
plus_dm[(up_move > down_move) & (up_move > 0)] = up_move
minus_dm[(down_move > up_move) & (down_move > 0)] = down_move
atr_smooth = tr.ewm(alpha=1/period, adjust=False).mean()
plus_di = 100 * plus_dm.ewm(alpha=1/period, adjust=False).mean() / atr_smooth
minus_di = 100 * minus_dm.ewm(alpha=1/period, adjust=False).mean() / atr_smooth
dx = 100 * (plus_di - minus_di).abs() / (plus_di + minus_di)
adx = dx.ewm(alpha=1/period, adjust=False).mean()
return pd.DataFrame({"plus_di": plus_di, "minus_di": minus_di, "adx": adx}, index=df.index)
Reading the three lines
- +DI / -DI — directional indicators; +DI above -DI means upward pressure dominates, and vice versa. Their crossover is itself a tradeable trend-direction signal (similar structure to chapter 102's MACD crossover).
- ADX — trend *strength*, direction-agnostic (it doesn't tell you up or down, only how strongly *whichever* direction is trending).
Standard ADX interpretation bands
def trend_strength_label(adx_value: float) -> str:
if adx_value < 20:
return "no trend / ranging"
elif adx_value < 40:
return "developing or moderate trend"
else:
return "strong trend"
The primary use: gating trend-following signals
def gated_macd_signal(macd_df: pd.DataFrame, adx_df: pd.DataFrame, adx_threshold: float = 20) -> pd.Series:
raw_signal = macd_crossover_signal(macd_df) # chapter 102
trending = adx_df["adx"] > adx_threshold
return raw_signal.where(trending, 0) # suppress signals when ADX says there's no trend to follow
This .where(condition, 0) pattern — take a raw signal, zero it out when a filter condition fails — is the simplest form of the AND-based condition combination formalized in chapter 115.
Common mistake: using ADX to time entries directly
ADX rising from 15 to 25 doesn't tell you *when* within that rise to enter — it's a filter/context indicator, not an entry trigger on its own. Pair it with a directional signal (MACD, EMA crossover, +DI/-DI crossover) that provides the actual entry timing.
Next: 107 — ATR and Supertrend