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
-
Python Standard Library — statistics
— population standard deviation (
pstdev) versus sample standard deviation (stdev). - NIST/SEMATECH e-Handbook of Statistical Methods — sample standard deviation and the N-1 denominator used when estimating population dispersion from sample data.
- John Bollinger — official Bollinger Bands explanation — traditional Bollinger Bands use a moving average and population standard deviation.