What Is Directional Movement? Build +DI and -DI with Python

AAPL candlestick chart with Directional Movement +DI -DI calculated in Python

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