ATR answered one useful question:
How much is price moving?
But ATR deliberately ignored direction.
A large True Range can happen during a strong rise, a strong fall, or a violent back-and-forth market.
So the next question is:
Which direction is showing more movement?
J. Welles Wilder built another set of measurements for exactly this problem.
+DM
-DM
↓
smooth
↓
compare with True Range
↓
+DI
-DI
These are the building blocks that eventually lead to ADX.
In this lesson, we will stop before ADX.
First, we need to understand what directional movement actually measures.
1. ATR Measures Movement Without Asking Which Side Won
True Range looks at three possible distances:
High - Low
| High - previous Close |
| Low - previous Close |
Then it keeps the largest one.
That is useful because gaps can make today's High-Low range underestimate the real move from the previous session.
But notice what True Range does not ask:
Did the market extend farther upward?
or
Did the market extend farther downward?
Directional Movement adds that question.
2. Start by Comparing High with High and Low with Low
Suppose we have two neighboring bars.
First compare today's High with yesterday's High:
Up Move
=
today's High
-
yesterday's High
Then compare yesterday's Low with today's Low:
Down Move
=
yesterday's Low
-
today's Low
These two numbers describe whether the price range expanded beyond the previous bar on the upper side or the lower side.
A larger new High creates a positive Up Move.
A lower new Low creates a positive Down Move.
3. Wilder Does Not Keep Both Sides on the Same Bar
This is the first rule that can feel unusual.
Wilder compares the two movements.
if Up Move > Down Move
and Up Move > 0
→ +DM = Up Move
otherwise
→ +DM = 0
For the downward side:
if Down Move > Up Move
and Down Move > 0
→ -DM = Down Move
otherwise
→ -DM = 0
So one bar usually contributes to one side or neither side.
upward expansion wins
→ +DM
downward expansion wins
→ -DM
neither side clearly wins
→ both 0
This rule prevents one unusually wide bar from automatically counting as both positive and negative directional movement.
4. Work Through Three Tiny Examples
Example A — Upward Expansion Wins
Yesterday
High = 100
Low = 95
Today
High = 103
Low = 96
Calculate:
Up Move
= 103 - 100
= 3
Down Move
= 95 - 96
= -1
The upward side wins.
+DM = 3
-DM = 0
Example B — Downward Expansion Wins
Yesterday
High = 100
Low = 95
Today
High = 99
Low = 91
Calculate:
Up Move
= 99 - 100
= -1
Down Move
= 95 - 91
= 4
The downward side wins.
+DM = 0
-DM = 4
Example C — Neither Side Wins
Yesterday
High = 100
Low = 95
Today
High = 99
Low = 96
Today's range stayed inside yesterday's range.
Up Move = -1
Down Move = -1
+DM = 0
-DM = 0
Directional Movement is not asking whether today's Close went up or down.
It is asking which side of the price range expanded beyond the previous bar.
5. Why Raw +DM and -DM Are Not Enough
Suppose:
Stock A
+DM = 2
Stock B
+DM = 2
Are those movements equally important?
Not necessarily.
A two-dollar directional move can be large in a quiet market and small in a very volatile market.
Wilder therefore compared directional movement with the market's range.
This is where the ATR lesson becomes useful again.
6. Reuse True Range as the Scale
We already know how to measure True Range.
Now we smooth three series with the same Wilder logic:
True Range
+DM
-DM
Then:
+DI
=
100 ×
smoothed +DM
/
smoothed True Range
and:
-DI
=
100 ×
smoothed -DM
/
smoothed True Range
DI means Directional Indicator.
We now have two normalized measurements:
+DI
→ relative strength of upward directional movement
-DI
→ relative strength of downward directional movement
The important word is relative.
Directional movement is being compared with the amount of range the market has recently produced.
7. We Can Reuse Wilder Smoothing
Wilder used the same basic recursive idea across several indicators.
After the first average:
new smoothed value
=
(
previous smoothed value × (period - 1)
+
today's value
)
/
period
This is the same memory structure we use for ATR.
old information
→ fades gradually
new information
→ enters gradually
That is useful for learning because we do not need a completely new smoothing method.
We reuse a known block.
8. First Build One-Bar Directional Movement
The first new Python function is small.
def directional_movement(
current_high,
current_low,
previous_high,
previous_low,
):
up_move = (
float(current_high)
- float(previous_high)
)
down_move = (
float(previous_low)
- float(current_low)
)
plus_dm = 0.0
minus_dm = 0.0
if (
up_move > down_move
and up_move > 0
):
plus_dm = up_move
elif (
down_move > up_move
and down_move > 0
):
minus_dm = down_move
return plus_dm, minus_dm
Read it as a decision tree.
measure upward expansion
↓
measure downward expansion
↓
compare them
↓
keep only the winning positive side
9. The Wilder Average Function Can Stay General
We will use one reusable smoothing function for:
True Range
+DM
-DM
def wilder_average(values, period):
result = [None] * len(values)
if len(values) <= period:
return result
first_values = values[1 : period + 1]
first_average = (
sum(first_values)
/ period
)
result[period] = first_average
for i in range(
period + 1,
len(values),
):
result[i] = (
result[i - 1] * (period - 1)
+ values[i]
) / period
return result
The first row has no previous bar, so our one-bar True Range and Directional Movement series begin at row 1.
Then the first 14-period smoothed value appears after enough observations exist.
10. Build +DI and -DI
Once we have the three smoothed series, the DI calculation is short.
plus_di
=
100
× smoothed_plus_dm
/ atr
minus_di
=
100
× smoothed_minus_dm
/ atr
Notice what happened.
ATR lesson
→ gave us the denominator
Directional Movement
→ gives us the directional numerator
together
→ +DI and -DI
11. Complete Python Program
Create a new file:
phase3_directional_movement_plus_minus_di.py
Copy the complete code below.
The price panel uses seagreen for rising candles
and firebrick for falling candles so the OHLC movement is easy to distinguish.
from pathlib import Path
from datetime import date, timedelta
import os
import FinanceDataReader as fdr
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
# ============================================================
# Phase 3
# Directional Movement: +DM, -DM, +DI, -DI
# ============================================================
# ------------------------------------------------------------
# 1. Settings
# ------------------------------------------------------------
symbol = "AAPL"
recent_trading_days = 160
di_period = 14
# Candlestick colors
bullish_color = "seagreen"
bearish_color = "firebrick"
# ------------------------------------------------------------
# 2. Working folder
# ------------------------------------------------------------
SCRIPT_DIR = Path(__file__).resolve().parent
os.chdir(SCRIPT_DIR)
print("Working folder:")
print(SCRIPT_DIR)
print()
# ------------------------------------------------------------
# 3. True Range
# ------------------------------------------------------------
def true_range(
current_high,
current_low,
previous_close,
):
high_low = (
float(current_high)
- float(current_low)
)
high_previous_close = abs(
float(current_high)
- float(previous_close)
)
low_previous_close = abs(
float(current_low)
- float(previous_close)
)
return max(
high_low,
high_previous_close,
low_previous_close,
)
# ------------------------------------------------------------
# 4. One-bar Directional Movement
# ------------------------------------------------------------
def directional_movement(
current_high,
current_low,
previous_high,
previous_low,
):
up_move = (
float(current_high)
- float(previous_high)
)
down_move = (
float(previous_low)
- float(current_low)
)
plus_dm = 0.0
minus_dm = 0.0
if (
up_move > down_move
and up_move > 0
):
plus_dm = up_move
elif (
down_move > up_move
and down_move > 0
):
minus_dm = down_move
return plus_dm, minus_dm
# ------------------------------------------------------------
# 5. Wilder average
# ------------------------------------------------------------
def wilder_average(
values,
period,
):
result = [None] * len(values)
if len(values) <= period:
return result
first_values = values[
1 : period + 1
]
first_average = (
sum(first_values)
/ period
)
result[period] = first_average
for i in range(
period + 1,
len(values),
):
result[i] = (
result[i - 1]
* (period - 1)
+ values[i]
) / period
return result
# ------------------------------------------------------------
# 6. Draw candlesticks
# ------------------------------------------------------------
def draw_candlesticks(
ax,
price_df,
width=0.60,
):
x_values = mdates.date2num(
price_df.index.to_pydatetime()
)
for x_value, (_, row) in zip(
x_values,
price_df.iterrows(),
):
open_price = float(row["Open"])
high_price = float(row["High"])
low_price = float(row["Low"])
close_price = float(row["Close"])
if close_price >= open_price:
candle_color = bullish_color
else:
candle_color = bearish_color
ax.vlines(
x=x_value,
ymin=low_price,
ymax=high_price,
color=candle_color,
linewidth=1.0,
)
body_bottom = min(
open_price,
close_price,
)
body_height = abs(
close_price
- open_price
)
if body_height == 0:
body_height = max(
(high_price - low_price) * 0.02,
0.001,
)
body = Rectangle(
(
x_value - width / 2,
body_bottom,
),
width,
body_height,
facecolor=candle_color,
edgecolor=candle_color,
linewidth=1.0,
)
ax.add_patch(body)
ax.xaxis_date()
# ------------------------------------------------------------
# 7. Download recent market data
# ------------------------------------------------------------
today = date.today()
start_date = (
today
- timedelta(days=300)
).strftime("%Y-%m-%d")
end_date = today.strftime("%Y-%m-%d")
df = fdr.DataReader(
symbol,
start_date,
end_date,
)
df = df.tail(
recent_trading_days
).copy()
if len(df) <= di_period:
raise ValueError(
"Not enough market data "
"for the selected DI period."
)
# ------------------------------------------------------------
# 8. Build one-bar TR, +DM, and -DM
# ------------------------------------------------------------
true_ranges = [None]
plus_dm_values = [None]
minus_dm_values = [None]
for i in range(
1,
len(df),
):
current_row = df.iloc[i]
previous_row = df.iloc[i - 1]
tr_value = true_range(
current_high=current_row["High"],
current_low=current_row["Low"],
previous_close=previous_row["Close"],
)
plus_dm, minus_dm = directional_movement(
current_high=current_row["High"],
current_low=current_row["Low"],
previous_high=previous_row["High"],
previous_low=previous_row["Low"],
)
true_ranges.append(tr_value)
plus_dm_values.append(plus_dm)
minus_dm_values.append(minus_dm)
# ------------------------------------------------------------
# 9. Smooth all three series
# ------------------------------------------------------------
atr_values = wilder_average(
values=true_ranges,
period=di_period,
)
smoothed_plus_dm = wilder_average(
values=plus_dm_values,
period=di_period,
)
smoothed_minus_dm = wilder_average(
values=minus_dm_values,
period=di_period,
)
# ------------------------------------------------------------
# 10. Build +DI and -DI
# ------------------------------------------------------------
plus_di_values = [None] * len(df)
minus_di_values = [None] * len(df)
for i in range(len(df)):
atr_value = atr_values[i]
if (
atr_value is None
or atr_value == 0
):
continue
plus_di_values[i] = (
100.0
* smoothed_plus_dm[i]
/ atr_value
)
minus_di_values[i] = (
100.0
* smoothed_minus_dm[i]
/ atr_value
)
# ------------------------------------------------------------
# 11. Add the results to the DataFrame
# ------------------------------------------------------------
df["ATR"] = atr_values
df["+DI"] = plus_di_values
df["-DI"] = minus_di_values
# ------------------------------------------------------------
# 12. Print the latest valid result
# ------------------------------------------------------------
valid_df = df.dropna(
subset=[
"ATR",
"+DI",
"-DI",
]
)
latest = valid_df.iloc[-1]
latest_date = valid_df.index[-1]
print("Directional Movement")
print("====================")
print()
print(
"Symbol:",
symbol,
)
print(
"Date:",
latest_date.strftime("%Y-%m-%d"),
)
print(
"Close:",
f'{latest["Close"]:.2f}',
)
print(
"ATR:",
f'{latest["ATR"]:.2f}',
)
print(
"+DI:",
f'{latest["+DI"]:.2f}',
)
print(
"-DI:",
f'{latest["-DI"]:.2f}',
)
print()
if latest["+DI"] > latest["-DI"]:
print(
"Directional dominance: upward"
)
elif latest["-DI"] > latest["+DI"]:
print(
"Directional dominance: downward"
)
else:
print(
"Directional dominance: equal"
)
# ------------------------------------------------------------
# 13. Plot candlesticks, +DI, and -DI
# ------------------------------------------------------------
plot_df = valid_df.tail(100)
fig, axes = plt.subplots(
nrows=2,
ncols=1,
figsize=(11, 7),
sharex=True,
height_ratios=[2, 1],
)
price_ax = axes[0]
di_ax = axes[1]
draw_candlesticks(
ax=price_ax,
price_df=plot_df,
)
price_ax.set_title(
f"{symbol} — Directional Movement"
)
price_ax.set_ylabel(
"Price"
)
price_ax.grid(
axis="y",
alpha=0.20,
)
di_ax.plot(
plot_df.index,
plot_df["+DI"],
label="+DI",
)
di_ax.plot(
plot_df.index,
plot_df["-DI"],
label="-DI",
)
di_ax.set_ylabel(
"Directional Indicator"
)
di_ax.grid(
axis="y",
alpha=0.20,
)
di_ax.legend()
di_ax.xaxis.set_major_formatter(
mdates.DateFormatter("%Y-%m-%d")
)
fig.autofmt_xdate()
fig.subplots_adjust(
left=0.10,
right=0.97,
top=0.92,
bottom=0.14,
hspace=0.08,
)
# ------------------------------------------------------------
# 14. Save the chart
# ------------------------------------------------------------
output_file = (
SCRIPT_DIR
/ "directional_movement_plus_minus_di.png"
)
fig.savefig(
output_file,
dpi=140,
)
print()
print("Chart saved:")
print(output_file)
plt.show()
plt.close(fig)
12. Run the Program
In the terminal:
python phase3_directional_movement_plus_minus_di.py
The program will:
download recent AAPL data
→ calculate True Range
→ calculate +DM and -DM
→ Wilder-smooth all three series
→ build +DI and -DI
→ print the latest values
→ draw green/red candlesticks with +DI and -DI
→ save the PNG
The image is saved as:
directional_movement_plus_minus_di.png
13. How to Read +DI and -DI
Start with the simplest relationship.
+DI > -DI
→ upward directional movement
is stronger than downward directional movement
The opposite:
-DI > +DI
→ downward directional movement
is stronger than upward directional movement
If the lines are close together, neither side has a large advantage under this measurement.
Notice what we still have not measured.
How strong is the trend overall?
That is the next problem.
14. A +DI / -DI Crossover Is Not Automatically a Trade
It is tempting to write:
+DI crosses above -DI
→ buy
-DI crosses above +DI
→ sell
But the formula does not contain an entry rule.
A crossover tells us that the measured directional balance changed.
It does not tell us:
- whether the trend is strong,
- whether the market is ranging,
- whether the crossover will persist,
- whether transaction costs matter,
- or whether the rule has worked out of sample.
So keep the distinction:
+DI / -DI
→ measurement
crossover entry rule
→ hypothesis to test
15. Direction and Strength Are Different Questions
This distinction prepares us for ADX.
+DI and -DI answer:
Which directional side is stronger?
ADX will answer a different question:
How strong is the directional separation,
regardless of which side is winning?
That means:
+DI / -DI
→ direction
ADX
→ strength
A market can therefore have:
+DI > -DI
but
weak overall directional separation
or:
-DI > +DI
and
strong directional separation
We need another calculation before we can measure that difference cleanly.
16. Change One Thing Yourself
Start with:
di_period = 14
Then try:
di_period = 7
Run the program again.
Ask:
Do +DI and -DI react faster?
Do they cross more often?
Do the lines look less smooth?
Then try:
di_period = 28
Ask the opposite questions.
Do not search for the best period yet.
First understand what smoothing does to the measurement.
17. One Implementation Detail to Remember
Different technical-analysis libraries can show slightly different values near the beginning of a series.
The reason is often initialization or rounding.
Our educational implementation is explicit:
first Wilder value
→ simple average of the first period observations
later values
→ recursive Wilder update
We keep that rule visible rather than changing it silently to match the earliest rows of another implementation.
Once enough observations have passed, implementations using the same underlying method should become much closer.
Check Your Understanding
- ATR measures the size of movement without telling us which direction won.
- Up Move compares today's High with the previous High.
- Down Move compares the previous Low with today's Low.
- Only the larger positive directional movement is kept on a bar; otherwise that side becomes zero.
- +DM and -DM are raw directional movements.
- +DI and -DI normalize smoothed directional movement by smoothed True Range.
- +DI above -DI means upward directional movement is stronger under this measurement.
- -DI above +DI means downward directional movement is stronger under this measurement.
- A DI crossover is not automatically a profitable entry signal.
- Direction and trend strength are separate questions.
What You Just Learned
High and Low
↓
compare with previous bar
↓
+DM -DM
\ /
\ /
Wilder smoothing
↓
True Range / ATR
↓
+DI -DI
↓
directional dominance
If one idea stays in your head after this lesson, let it be this:
+DI and -DI do not predict direction. They measure which side of recent directional movement is stronger after adjusting for the market's range.
Where Do We Go Next?
We now have two lines.
+DI
-DI
The next question is not which one is higher.
It is:
How far apart are they
relative to their combined size?
That produces DX.
Smooth DX with Wilder's method, and we arrive at one of technical analysis's most widely used trend-strength measurements:
ADX
In the next lesson, we will build it from the blocks we now understand.
Sources
- J. Welles Wilder, New Concepts in Technical Trading Systems, Trend Research, 1978 — Google Books bibliographic record.
- TA-Lib — Plus Directional Movement (+DM).
- TA-Lib — Minus Directional Movement (-DM).
- TA-Lib — Plus Directional Indicator (+DI).
- TA-Lib — Minus Directional Indicator (-DI).