What Is Standard Deviation? Measure Price Dispersion with Python

AAPL candlestick chart with 20-day mean and rolling population standard deviation calculated in Python

We just finished a family of indicators built around range and directional movement.

True Range
→ ATR
→ Directional Movement
→ ADX

Those calculations taught us one way to describe market movement. Now we will ask a different statistical question:

How spread out are
recent prices
around their average?

That question leads to standard deviation.

Standard deviation is not a trading signal. It is a general statistical measure of dispersion. In this lesson, we will build it from scratch and then apply it to rolling Close prices.

1. Start with the Mean

Suppose five Close prices are:

100
102
104
106
108

The arithmetic mean is:

mean
=
(100 + 102 + 104 + 106 + 108)
/
5

=
104

The mean gives us a center. But it does not tell us how tightly the observations cluster around that center.

2. Measure Each Distance from the Mean

100 - 104 = -4
102 - 104 = -2
104 - 104 =  0
106 - 104 = +2
108 - 104 = +4

These are deviations from the mean.

value
-
mean
=
deviation

3. Why Can’t We Just Average the Deviations?

-4 + -2 + 0 + 2 + 4
=
0

Positive and negative deviations cancel. So their ordinary average cannot describe spread.

We need a transformation that makes both sides contribute positively.

4. Square Each Deviation

deviation     squared deviation

-4            16
-2             4
 0             0
+2             4
+4            16

Add the squared deviations:

16 + 4 + 0 + 4 + 16
=
40

The sign-cancellation problem is gone.

5. Variance Is the Average Squared Distance

For the population-style calculation used in this lesson:

variance
=
sum of squared deviations
/
N

Therefore:

variance
=
40 / 5
=
8

Variance is useful mathematically, but its units are squared price units.

6. Standard Deviation Brings the Units Back

standard deviation
=
√variance

=
√8

≈
2.828427

We started with prices. Standard deviation returns to price units.

price
→ deviation
→ squared deviation
→ variance
→ square root
→ standard deviation

7. Read Standard Deviation as Dispersion

values cluster near mean
→ smaller standard deviation

values spread farther from mean
→ larger standard deviation

Standard deviation does not tell us whether price is bullish or bearish.

8. Population or Sample Standard Deviation?

A population standard deviation divides the sum of squared deviations by:

N

A sample standard deviation commonly used to estimate a larger population divides by:

N - 1

Python's standard library makes the distinction explicit: statistics.pstdev() calculates population standard deviation, while statistics.stdev() calculates sample standard deviation.

In this lesson, each rolling price window is treated as the complete set of values we want to describe, so our function uses the population formula.

9. Why Use the Population Formula Here?

The next lesson will build Bollinger Bands.

John Bollinger's official explanation states that the traditional construction uses a simple moving average and the population calculation for standard deviation.

So this lesson deliberately builds the same standard-deviation block we will reuse next.

rolling Close prices
        ↓
population standard deviation
        ↓
reusable block
        ↓
Bollinger Bands

10. Standard Deviation and ATR Are Not the Same Volatility Measure

ATR
→ High
→ Low
→ Previous Close
→ bar-by-bar range magnitude
→ Wilder smoothing


Price Standard Deviation
→ rolling Close values
→ distance from their mean
→ dispersion across a window

They describe different properties and should not be treated as interchangeable.

11. Price Dispersion Is Not the Same as Return Volatility

In finance, the word volatility often refers to the standard deviation of returns.

return series
→ standard deviation
→ return volatility

That is not what we calculate here.

Close prices
→ rolling standard deviation
→ price dispersion

We choose price dispersion because it is the building block needed for the traditional Bollinger Bands calculation.

12. A Smooth Trend Can Still Increase Price Standard Deviation

Standard deviation does not mean randomness.

Consider the orderly sequence:

100
102
104
106
108

The observations are still spread around their mean. So price-level standard deviation can be elevated even when prices move smoothly in one direction.

This is another reason not to casually equate price dispersion with return volatility.

13. Build the Mean from Scratch

def arithmetic_mean(values):
    if len(values) == 0:
        raise ValueError(
            "values must not be empty"
        )

    return (
        sum(float(value) for value in values)
        / len(values)
    )

14. Build Population Variance from Scratch

def population_variance(values):
    mean_value = arithmetic_mean(
        values
    )

    squared_deviations = []

    for value in values:
        deviation = (
            float(value)
            - mean_value
        )

        squared_deviation = (
            deviation ** 2
        )

        squared_deviations.append(
            squared_deviation
        )

    return (
        sum(squared_deviations)
        / len(values)
    )

The Python mirrors the hand calculation.

15. Standard Deviation Is Only One More Step

def population_standard_deviation(
    values,
):
    variance_value = (
        population_variance(values)
    )

    return math.sqrt(
        variance_value
    )

math is part of Python's standard library, so it does not require a separate package installation. We use math.sqrt() only to take the square root.

16. Turn One Calculation into a Rolling Indicator

first 20 Close prices
→ StdDev 20

move forward one bar

next 20 Close prices
→ StdDev 20

move forward again
→ repeat

The first valid 20-period value appears only after 20 Close prices are available.

17. Build Rolling Standard Deviation

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

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

        result[i] = (
            population_standard_deviation(
                window
            )
        )

    return result
fixed calculation
+
sliding window
=
rolling indicator

18. Validate Before Downloading Market Data

values
=
100, 102, 104, 106, 108

mean
=
104

population variance
=
8

population standard deviation
=
√8
≈
2.828427

If those values do not match, the script stops before interpreting real market data.

19. What the AAPL Chart Shows

Panel 1
AAPL candlesticks
+
20-day mean

Panel 2
20-day rolling
population standard deviation

Rising candles use seagreen. Falling candles use firebrick.

The mean and standard-deviation lines use neutral colors because dispersion itself has no bullish or bearish direction.

20. What Should You Look for?

tighter cluster around mean
→ lower standard deviation

wider dispersion around mean
→ higher standard deviation

Do not immediately turn either observation into a buy or sell rule.

21. Change One Thing Yourself

Start with:

standard_deviation_period = 20

Then try:

standard_deviation_period = 10

Ask:

Does the line react faster?

Does it change more sharply?

Then try:

standard_deviation_period = 40

Ask the opposite questions. The goal is not to optimize the period yet.

22. Standard Deviation Is Not a Trading Strategy

standard deviation rises
does not automatically mean
buy

standard deviation falls
does not automatically mean
sell

Standard deviation describes dispersion. Any trading rule built from it must be defined separately and tested.

23. The Complete Python Program

This lesson introduces no new external Python package. We reuse FinanceDataReader and matplotlib. Python's built-in math module supplies the square-root function.

Save the following file as:

phase3_standard_deviation.py
from pathlib import Path
from datetime import date, timedelta
import math
import os

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


# ============================================================
# Phase 3-22 — Standard Deviation
# Measure rolling price dispersion from scratch
# ============================================================


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

symbol = "AAPL"
recent_trading_days = 180
standard_deviation_period = 20

bullish_color = "seagreen"
bearish_color = "firebrick"

mean_color = "dimgray"
standard_deviation_color = "black"


# ------------------------------------------------------------
# 2. Working folder
# ------------------------------------------------------------

SCRIPT_DIR = Path(__file__).resolve().parent
os.chdir(SCRIPT_DIR)

print("Working folder:")
print(SCRIPT_DIR)
print()


# ------------------------------------------------------------
# 3. Arithmetic mean
# ------------------------------------------------------------

def arithmetic_mean(values):
    if len(values) == 0:
        raise ValueError("values must not be empty")

    return (
        sum(float(value) for value in values)
        / len(values)
    )


# ------------------------------------------------------------
# 4. Population variance
# ------------------------------------------------------------

def population_variance(values):
    if len(values) == 0:
        raise ValueError("values must not be empty")

    mean_value = arithmetic_mean(values)

    squared_deviations = []

    for value in values:
        deviation = float(value) - mean_value
        squared_deviation = deviation ** 2

        squared_deviations.append(
            squared_deviation
        )

    return (
        sum(squared_deviations)
        / len(values)
    )


# ------------------------------------------------------------
# 5. Population standard deviation
# ------------------------------------------------------------

def population_standard_deviation(values):
    variance_value = population_variance(
        values
    )

    return math.sqrt(
        variance_value
    )


# ------------------------------------------------------------
# 6. Rolling mean
# ------------------------------------------------------------

def rolling_mean(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
        ]

        result[i] = arithmetic_mean(
            window
        )

    return result


# ------------------------------------------------------------
# 7. Rolling population standard deviation
# ------------------------------------------------------------

def rolling_population_standard_deviation(
    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
        ]

        result[i] = (
            population_standard_deviation(
                window
            )
        )

    return result


# ------------------------------------------------------------
# 8. 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)


# ------------------------------------------------------------
# 9. Hand-calculation self-test
# ------------------------------------------------------------

toy_values = [
    100.0,
    102.0,
    104.0,
    106.0,
    108.0,
]

toy_mean = arithmetic_mean(
    toy_values
)

toy_variance = population_variance(
    toy_values
)

toy_standard_deviation = (
    population_standard_deviation(
        toy_values
    )
)

expected_mean = 104.0
expected_variance = 8.0
expected_standard_deviation = math.sqrt(
    8.0
)

assert abs(
    toy_mean
    - expected_mean
) < 1e-12

assert abs(
    toy_variance
    - expected_variance
) < 1e-12

assert abs(
    toy_standard_deviation
    - expected_standard_deviation
) < 1e-12

toy_rolling = (
    rolling_population_standard_deviation(
        values=[
            100.0,
            102.0,
            104.0,
            106.0,
            108.0,
            110.0,
        ],
        period=5,
    )
)

assert toy_rolling[:4] == [
    None,
    None,
    None,
    None,
]

assert abs(
    toy_rolling[4]
    - math.sqrt(8.0)
) < 1e-12

assert abs(
    toy_rolling[5]
    - math.sqrt(8.0)
) < 1e-12

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

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

print(
    "Mean:",
    f"{toy_mean:.6f}",
)

print(
    "Population variance:",
    f"{toy_variance:.6f}",
)

print(
    "Population standard deviation:",
    f"{toy_standard_deviation:.6f}",
)

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


# ------------------------------------------------------------
# 10. Download recent market data
# ------------------------------------------------------------

today = date.today()

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


# ------------------------------------------------------------
# 11. Build rolling mean and standard deviation
# ------------------------------------------------------------

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

mean_values = rolling_mean(
    values=close_values,
    period=standard_deviation_period,
)

standard_deviation_values = (
    rolling_population_standard_deviation(
        values=close_values,
        period=standard_deviation_period,
    )
)

df["Mean"] = mean_values
df["Standard Deviation"] = (
    standard_deviation_values
)


# ------------------------------------------------------------
# 12. Print the latest values
# ------------------------------------------------------------

valid_df = df.dropna(
    subset=[
        "Mean",
        "Standard Deviation",
    ]
)

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

print("Latest rolling price dispersion")
print("===============================")
print()

print(
    "Symbol:",
    symbol,
)

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

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

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

print(
    f"Population StdDev({standard_deviation_period}):",
    f'{latest["Standard Deviation"]:.4f}',
)


# ------------------------------------------------------------
# 13. Plot candles + rolling standard deviation
# ------------------------------------------------------------

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

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

fig = plt.figure(
    figsize=(12, 9),
)

grid = fig.add_gridspec(
    2,
    1,
    height_ratios=[2.1, 1.0],
    hspace=0.08,
)

ax_price = fig.add_subplot(
    grid[0]
)

ax_std = fig.add_subplot(
    grid[1],
    sharex=ax_price,
)

draw_candlesticks(
    ax=ax_price,
    market_df=plot_df,
)

ax_price.plot(
    x_values,
    plot_df["Mean"],
    color=mean_color,
    linewidth=1.6,
    label=(
        f"Mean "
        f"{standard_deviation_period}"
    ),
)

ax_price.set_title(
    f"{symbol} — Rolling Price Dispersion"
)

ax_price.set_ylabel(
    "Price"
)

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

ax_price.legend()

ax_price.tick_params(
    axis="x",
    labelbottom=False,
)

ax_std.plot(
    x_values,
    plot_df["Standard Deviation"],
    color=standard_deviation_color,
    linewidth=2.0,
    label=(
        "Population StdDev "
        f"{standard_deviation_period}"
    ),
)

ax_std.set_ylabel(
    "Price Units"
)

ax_std.set_xlabel(
    "Date"
)

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

ax_std.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_std.set_xticks(
    tick_positions
)

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

fig.subplots_adjust(
    left=0.09,
    right=0.98,
    top=0.94,
    bottom=0.13,
)


# ------------------------------------------------------------
# 14. Save the chart
# ------------------------------------------------------------

output_file = (
    SCRIPT_DIR
    / "standard_deviation_price_dispersion_aapl.png"
)

fig.savefig(
    output_file,
    dpi=140,
)

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

plt.show()
plt.close(fig)

24. Run the Program

python phase3_standard_deviation.py

First confirm:

Self-test: PASS

The program then downloads recent AAPL data, calculates the rolling 20-day mean and population standard deviation, and saves:

standard_deviation_price_dispersion_aapl.png

Check Your Understanding

  • The mean gives a center, while standard deviation describes spread around that center.
  • Raw deviations cannot simply be averaged because positive and negative values cancel.
  • Variance averages squared deviations.
  • Standard deviation is the square root of variance.
  • Population variance divides by N; sample variance commonly divides by N-1.
  • This lesson uses population standard deviation so the block matches the traditional Bollinger Bands construction used next.
  • Rolling standard deviation recalculates dispersion over a moving window.
  • Price-level standard deviation is not the same thing as the standard deviation of returns.
  • ATR and standard deviation use different inputs and transformations.
  • Standard deviation measures dispersion, not bullish or bearish direction.
  • A standard-deviation reading is not automatically a trading signal.

What You Just Learned

Close prices
     ↓
calculate mean
     ↓
distance from mean
     ↓
square each distance
     ↓
average squared distances
     ↓
variance
     ↓
square root
     ↓
standard deviation
     ↓
price dispersion

Standard deviation tells us how spread out a group of values is around its mean. It does not tell us which direction price will move.

Where Do We Go Next?

Simple Moving Average
→ center

Standard Deviation
→ dispersion

The next lesson combines them.

moving average
±
multiple of standard deviation
        ↓
Bollinger Bands

That will be another example of a more advanced indicator emerging from Building Blocks we already understand.

Sources