What Is Wilder’s Smoothing? Build Wilder Average from Scratch with Python

AAPL candlestick chart comparing SMA 14, EMA 14, and Wilder 14 smoothing calculated in Python We already know two ways to smooth market data.

SMA
→ average a fixed window

EMA
→ keep a running average
→ give more weight to recent values

But another smoothing rule has quietly appeared in our indicator lessons.

It is inside RSI.

It will appear again when we build ATR.

Then it will appear again in Directional Movement and ADX.

RSI
   \

True Range
   ↓
Wilder Smoothing
   ↓
ATR
   ↓
Directional Movement
   ↓
ADX

At this point, repeating the same formula inside every indicator would hide an important idea.

So we will stop and make the smoothing rule itself a reusable Learning Block.

1. What Is Wilder’s Smoothing?

J. Welles Wilder used a recursive averaging method in the indicator framework he published in New Concepts in Technical Trading Systems in 1978.

The method is often called:

Wilder's smoothing

Wilder average

RMA
→ Running Moving Average

SMMA
→ Smoothed Moving Average

Different platforms can use different names, but the central recursive idea is simple.

First create an initial average.

Then update that average using only:

previous average
+
new value

2. Start with a Simple Average

Suppose our period is:

N = 3

and the first three values are:

10, 13, 16

The first Wilder average is simply:

(10 + 13 + 16) / 3
= 13

So the recursive process needs a starting point.

first N values
→ ordinary arithmetic mean
→ first Wilder value

3. After the Seed, Do Not Recalculate the Whole Window

Now suppose the next value is:

25

Wilder's update can be written as:

New Average
=
(
Previous Average × (N - 1)
+
New Value
)
/
N

With our numbers:

(
13 × 2
+
25
)
/
3

=
17

The next input is:

22

Update again:

(
17 × 2
+
22
)
/
3

=
18.666667

Notice what we did not do.

We did not throw away the previous average and rebuild everything from scratch.

4. The Same Formula Has a More Revealing Form

We can rearrange the update:

New Average
=
Previous Average
+
(
New Value
-
Previous Average
)
/
N

This form makes the learning rate visible.

alpha
=
1 / N

So Wilder smoothing is a recursive exponential-style smoother with a smoothing factor of:

α = 1 / N

5. Compare Wilder Smoothing with a Standard EMA

The EMA we learned earlier commonly uses:

EMA alpha
=
2 / (N + 1)

Wilder smoothing uses:

Wilder alpha
=
1 / N

For a 14-period calculation:

EMA 14 alpha
=
2 / 15
≈ 0.133333

Wilder 14 alpha
=
1 / 14
≈ 0.071429

The Wilder value moves a smaller fraction toward each new observation.

So, with the same stated period, Wilder smoothing normally reacts more slowly than the common EMA formula.

6. Why People Sometimes Say Wilder 14 Is Similar to EMA 27

Ask which standard EMA period has the same alpha as Wilder 14.

2 / (M + 1)
=
1 / 14

Solve for M:

M
=
2 × 14 - 1
=
27

In general:

Wilder N
has the same alpha as
EMA (2N - 1)

That does not mean every Wilder implementation will exactly match every EMA implementation.

Initialization rules can differ.

But the equivalence helps us understand the smoothing speed.

7. Compare SMA, EMA, and Wilder with One Tiny Sequence

Use:

values
=
10, 13, 16, 25, 22

period
=
3

All three methods begin with the same seed in this lesson:

(10 + 13 + 16) / 3
=
13

At the next value, 25:

SMA 3
=
(13 + 16 + 25) / 3
=
18

EMA 3
=
13 + 0.5 × (25 - 13)
=
19

Wilder 3
=
13 + (1/3) × (25 - 13)
=
17

At the final value, 22:

SMA 3
=
(16 + 25 + 22) / 3
=
21

EMA 3
=
19 + 0.5 × (22 - 19)
=
20.5

Wilder 3
=
17 + (1/3) × (22 - 17)
≈
18.666667

The three methods are not interchangeable.

SMA
→ fixed window

EMA
→ recursive
→ alpha = 2 / (N + 1)

Wilder
→ recursive
→ alpha = 1 / N

8. Wilder Smoothing Has Memory

The previous average already contains information from older observations.

So when we write:

new average
=
old average
+
small adjustment

old information does not suddenly disappear.

Its influence fades gradually.

old observations
→ stored inside previous average
→ influence fades over time

new observation
→ enters with weight 1 / N

This is why the word recursive matters.

9. Build a Reusable Wilder Function

We want one function that can later accept:

price values

RSI gains

RSI losses

True Range

+DM

-DM

DX

Some of those series begin immediately.

Others, such as True Range, can begin with one missing value because the first row has no previous Close.

So our function allows leading None values and begins the seed at the first valid observation.

def wilder_average(values, period):
    result = [None] * len(values)

    if period <= 0:
        raise ValueError("period must be positive")

    valid_start = next(
        (
            i
            for i, value in enumerate(values)
            if value is not None
        ),
        None,
    )

    if valid_start is None:
        return result

    seed_end = valid_start + period - 1

    if seed_end >= len(values):
        return result

    seed_values = values[
        valid_start : seed_end + 1
    ]

    if any(
        value is None
        for value in seed_values
    ):
        raise ValueError(
            "Expected a continuous block "
            "of valid values for the Wilder seed."
        )

    seed = (
        sum(float(value) for value in seed_values)
        / period
    )

    result[seed_end] = seed

    alpha = 1.0 / period

    for i in range(
        seed_end + 1,
        len(values),
    ):
        current_value = values[i]

        if current_value is None:
            result[i] = None
            continue

        previous_average = result[i - 1]

        if previous_average is None:
            raise ValueError(
                "Unexpected missing Wilder state."
            )

        result[i] = (
            previous_average
            + alpha
            * (
                float(current_value)
                - previous_average
            )
        )

    return result

10. Read the Function as a Learning Flow

find first valid value
        ↓
collect N valid observations
        ↓
ordinary mean
        ↓
first Wilder value
        ↓
take next observation
        ↓
move 1/N of the distance
toward the new value
        ↓
repeat

Once this block is understood, later indicators become shorter.

11. The Same Function Can Handle True Range

Our True Range series begins like this:

None,
TR 1,
TR 2,
TR 3,
...

That first None is not an error.

The first bar has no previous Close inside the dataset.

The reusable function simply finds the first valid True Range and begins counting the smoothing period there.

True Range
→ wilder_average()
→ ATR

12. Why RSI Needs This Block

RSI first separates price changes into:

gains
losses

Those raw series are noisy.

Wilder's RSI smooths the gain and loss measurements before comparing them.

gains
→ Wilder smoothing
→ average gain

losses
→ Wilder smoothing
→ average loss

average gain / average loss
→ RS
→ RSI

If you want to revisit that indicator, see What Is RSI? Compare Recent Gains and Losses with Python.

13. Why ATR Needs This Block

True Range gives one movement measurement per bar.

ATR asks for a smoother view across time.

True Range
→ Wilder smoothing
→ Average True Range

That is why this lesson belongs naturally between True Range and ATR.

14. Directional Movement Reuses It Again

Directional Movement creates:

+DM
-DM

Those series are also smoothed before they are normalized by range.

+DM
→ Wilder smoothing

-DM
→ Wilder smoothing

True Range
→ Wilder smoothing

then
→ +DI / -DI

So the same averaging block becomes shared infrastructure.

15. ADX Reuses the Same Idea Yet Again

After +DI and -DI, we can calculate DX.

Then:

DX
→ Wilder smoothing
→ ADX

This is the reason we do not want a new smoothing function inside every article.

learn once
→ test once
→ freeze the block
→ reuse it

16. Initialization Is Part of the Definition

Recursive averages need a starting value.

Our educational implementation uses:

first Wilder value
=
simple mean of the first N valid observations

After that:

recursive Wilder update

Other software can use different warm-up or initialization conventions.

That can make the earliest values differ.

So when two libraries disagree near the beginning of a series, do not immediately assume one formula is wrong.

Check the seed first.

17. The Complete Python Program

This lesson introduces no new Python package.

We reuse FinanceDataReader for market data and matplotlib for the chart.

Save the following file as:

phase3_wilder_smoothing.py
from pathlib import Path
from datetime import date, timedelta
import os

import FinanceDataReader as fdr
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle


# ============================================================
# Phase 3 — Wilder's Smoothing
# Compare SMA, standard EMA, and Wilder Average
# ============================================================


# ------------------------------------------------------------
# 1. Settings
# ------------------------------------------------------------

symbol = "AAPL"

recent_trading_days = 140

smoothing_period = 14

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. Simple moving average
# ------------------------------------------------------------

def simple_moving_average(values, period):
    result = [None] * len(values)

    if period <= 0:
        raise ValueError("period must be positive")

    if len(values) < period:
        return result

    for i in range(period - 1, len(values)):
        window = values[i - period + 1 : i + 1]

        if any(value is None for value in window):
            continue

        result[i] = (
            sum(float(value) for value in window)
            / period
        )

    return result


# ------------------------------------------------------------
# 4. Standard EMA with the same SMA seed
# ------------------------------------------------------------

def ema_with_sma_seed(values, period):
    result = [None] * len(values)

    if period <= 0:
        raise ValueError("period must be positive")

    valid_start = next(
        (
            i
            for i, value in enumerate(values)
            if value is not None
        ),
        None,
    )

    if valid_start is None:
        return result

    seed_end = valid_start + period - 1

    if seed_end >= len(values):
        return result

    seed_values = values[
        valid_start : seed_end + 1
    ]

    if any(
        value is None
        for value in seed_values
    ):
        raise ValueError(
            "Expected a continuous block "
            "of valid values for the EMA seed."
        )

    seed = (
        sum(float(value) for value in seed_values)
        / period
    )

    result[seed_end] = seed

    alpha = 2.0 / (period + 1.0)

    for i in range(
        seed_end + 1,
        len(values),
    ):
        current_value = values[i]

        if current_value is None:
            result[i] = None
            continue

        previous_average = result[i - 1]

        if previous_average is None:
            raise ValueError(
                "Unexpected missing EMA state."
            )

        result[i] = (
            previous_average
            + alpha
            * (
                float(current_value)
                - previous_average
            )
        )

    return result


# ------------------------------------------------------------
# 5. Wilder average
# ------------------------------------------------------------

def wilder_average(values, period):
    result = [None] * len(values)

    if period <= 0:
        raise ValueError("period must be positive")

    valid_start = next(
        (
            i
            for i, value in enumerate(values)
            if value is not None
        ),
        None,
    )

    if valid_start is None:
        return result

    seed_end = valid_start + period - 1

    if seed_end >= len(values):
        return result

    seed_values = values[
        valid_start : seed_end + 1
    ]

    if any(
        value is None
        for value in seed_values
    ):
        raise ValueError(
            "Expected a continuous block "
            "of valid values for the Wilder seed."
        )

    seed = (
        sum(float(value) for value in seed_values)
        / period
    )

    result[seed_end] = seed

    alpha = 1.0 / period

    for i in range(
        seed_end + 1,
        len(values),
    ):
        current_value = values[i]

        if current_value is None:
            result[i] = None
            continue

        previous_average = result[i - 1]

        if previous_average is None:
            raise ValueError(
                "Unexpected missing Wilder state."
            )

        result[i] = (
            previous_average
            + alpha
            * (
                float(current_value)
                - previous_average
            )
        )

    return result


# ------------------------------------------------------------
# 6. Draw candlesticks
# ------------------------------------------------------------

def draw_candlesticks(
    ax,
    market_df,
    body_width=0.62,
):
    for x, (_, row) in enumerate(
        market_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,
            low_price,
            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.01,
            ) * 0.02

        body = Rectangle(
            (
                x - body_width / 2.0,
                body_bottom,
            ),
            body_width,
            body_height,
            facecolor=candle_color,
            edgecolor=candle_color,
            linewidth=0.8,
        )

        ax.add_patch(body)


# ------------------------------------------------------------
# 7. Hand-calculation self-test
# ------------------------------------------------------------

toy_values = [
    10.0,
    13.0,
    16.0,
    25.0,
    22.0,
]

toy_period = 3

toy_sma = simple_moving_average(
    toy_values,
    toy_period,
)

toy_ema = ema_with_sma_seed(
    toy_values,
    toy_period,
)

toy_wilder = wilder_average(
    toy_values,
    toy_period,
)

expected_wilder = [
    None,
    None,
    13.0,
    17.0,
    18.666666666666668,
]

for actual, expected in zip(
    toy_wilder,
    expected_wilder,
):
    if expected is None:
        assert actual is None
    else:
        assert abs(
            actual - expected
        ) < 1e-12


# A second test proves the same function can
# accept one leading None, as True Range does.
tr_like_values = [
    None,
    6.0,
    4.0,
    5.0,
    8.0,
]

tr_like_wilder = wilder_average(
    tr_like_values,
    period=3,
)

assert tr_like_wilder[:3] == [
    None,
    None,
    None,
]

assert abs(
    tr_like_wilder[3] - 5.0
) < 1e-12

assert abs(
    tr_like_wilder[4]
    - 6.0
) < 1e-12


print("Self-test")
print("=========")
print()

print("Toy values:")
print(toy_values)
print()

print("SMA(3):")
print(toy_sma)
print()

print("EMA(3), alpha = 2 / (3 + 1):")
print(toy_ema)
print()

print("Wilder(3), alpha = 1 / 3:")
print(toy_wilder)
print()

print("Self-test: PASS")
print()


# ------------------------------------------------------------
# 8. Download recent market data
# ------------------------------------------------------------

today = date.today()

start_date = (
    today
    - timedelta(days=320)
).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) < smoothing_period:
    raise ValueError(
        "Not enough market data "
        "for the selected period."
    )


# ------------------------------------------------------------
# 9. Build the three smoothers
# ------------------------------------------------------------

close_values = [
    float(value)
    for value in df["Close"]
]

sma_values = simple_moving_average(
    values=close_values,
    period=smoothing_period,
)

ema_values = ema_with_sma_seed(
    values=close_values,
    period=smoothing_period,
)

wilder_values = wilder_average(
    values=close_values,
    period=smoothing_period,
)

df["SMA"] = sma_values
df["EMA"] = ema_values
df["Wilder"] = wilder_values


# ------------------------------------------------------------
# 10. Print the latest comparison
# ------------------------------------------------------------

valid_df = df.dropna(
    subset=[
        "SMA",
        "EMA",
        "Wilder",
    ]
)

latest = valid_df.iloc[-1]
latest_date = valid_df.index[-1]

print("Latest comparison")
print("=================")
print()

print(
    "Symbol:",
    symbol,
)

print(
    "Date:",
    latest_date.strftime("%Y-%m-%d"),
)

print(
    "Close:",
    f'{latest["Close"]:.2f}',
)

print(
    f"SMA({smoothing_period}):",
    f'{latest["SMA"]:.2f}',
)

print(
    f"EMA({smoothing_period}):",
    f'{latest["EMA"]:.2f}',
)

print(
    f"Wilder({smoothing_period}):",
    f'{latest["Wilder"]:.2f}',
)

print()

print(
    "Standard EMA alpha:",
    f'{2.0 / (smoothing_period + 1.0):.6f}',
)

print(
    "Wilder alpha:",
    f'{1.0 / smoothing_period:.6f}',
)

print(
    "EMA period with the same alpha "
    "as Wilder period:",
    2 * smoothing_period - 1,
)


# ------------------------------------------------------------
# 11. Plot candles and the three averages
# ------------------------------------------------------------

plot_df = df.tail(90).copy()

fig, ax = plt.subplots(
    figsize=(12, 7),
)

draw_candlesticks(
    ax,
    plot_df,
)

x_values = list(
    range(len(plot_df))
)

ax.plot(
    x_values,
    plot_df["SMA"],
    label=f"SMA {smoothing_period}",
    linewidth=1.4,
)

ax.plot(
    x_values,
    plot_df["EMA"],
    label=f"EMA {smoothing_period}",
    linewidth=1.4,
)

ax.plot(
    x_values,
    plot_df["Wilder"],
    label=f"Wilder {smoothing_period}",
    linewidth=1.6,
)

ax.set_title(
    f"{symbol} — SMA vs EMA vs Wilder Smoothing"
)

ax.set_ylabel(
    "Price"
)

ax.grid(
    axis="y",
    alpha=0.20,
)

ax.legend()

tick_step = max(
    1,
    len(plot_df) // 8,
)

tick_positions = list(
    range(
        0,
        len(plot_df),
        tick_step,
    )
)

tick_labels = [
    plot_df.index[i].strftime(
        "%Y-%m-%d"
    )
    for i in tick_positions
]

ax.set_xticks(
    tick_positions
)

ax.set_xticklabels(
    tick_labels,
    rotation=35,
    ha="right",
)

fig.subplots_adjust(
    left=0.09,
    right=0.98,
    top=0.92,
    bottom=0.17,
)


# ------------------------------------------------------------
# 12. Save the chart
# ------------------------------------------------------------

output_file = (
    SCRIPT_DIR
    / "wilder_smoothing_sma_ema_comparison.png"
)

fig.savefig(
    output_file,
    dpi=140,
)

print()
print("Chart saved:")
print(output_file)

plt.show()
plt.close(fig)

18. Run the Program

python phase3_wilder_smoothing.py

The program first checks the hand-calculation example.

It also checks that the same Wilder function works when the input begins with one None, like True Range.

Then it downloads recent AAPL data and compares:

SMA 14
EMA 14
Wilder 14

The chart is saved as:

wilder_smoothing_sma_ema_comparison.png

Rising candles use seagreen.

Falling candles use firebrick.

19. What Should You Look for on the Chart?

Do not ask which line predicts price best.

That is not the learning question.

Instead ask:

Which line reacts fastest
after a sudden move?

Which line stays smoother?

How long does each line
remember the move?

Because the standard EMA has the larger alpha, it will usually react faster than Wilder smoothing when both use the same stated period.

20. Change One Thing Yourself

Start with:

smoothing_period = 14

Then try:

smoothing_period = 7

Ask:

Does Wilder smoothing react faster?

Does the gap between
EMA and Wilder change?

Then try:

smoothing_period = 28

Ask the opposite questions.

Do not search for the best period yet.

First understand what the parameter changes.

21. Wilder Smoothing Is Not a Trading Signal

A smoother is a transformation.

input series
→ smoothing rule
→ smoother series

It does not automatically say:

buy
sell
trend continues
trend reverses

Wilder smoothing becomes useful because later indicators use it to transform specific measurements.

Check Your Understanding

  • Wilder smoothing begins with an arithmetic mean of the first N valid observations in our implementation.
  • After the seed, the calculation is recursive.
  • Wilder's smoothing factor is 1/N.
  • A common EMA uses 2/(N+1), so it reacts faster than Wilder smoothing for the same stated period.
  • Wilder N has the same alpha as a standard EMA with period 2N-1.
  • Recursive smoothing keeps older information inside the previous average and lets its influence fade gradually.
  • Initialization differences can create different early values between software implementations.
  • The same Wilder averaging block can be reused in RSI, ATR, Directional Movement, and ADX.
  • Wilder smoothing is a mathematical transformation, not a trading rule.

What You Just Learned

first N valid values
        ↓
simple average
        ↓
first Wilder value
        ↓
previous average
+
1/N of the gap
toward new value
        ↓
new Wilder value
        ↓
repeat

If one idea stays in your head after this lesson, let it be this:

Wilder smoothing is a reusable recursive averaging block. Learn it once, and RSI, ATR, DMI, and ADX become easier to understand.

Where Do We Go Next?

We already know how to calculate True Range.

We now know how Wilder smooths a sequence.

True Range
+
Wilder Smoothing
=
Average True Range

That is the next natural Building Block.

Sources