What Is a Moving Average? Build One from Prices with Python

Recent AAPL closing prices with a five-day simple moving average calculated from scratch in Python

Price moves up and down every day. That can make the chart look noisy.

A moving average asks a simple question:

What is the average price
of the most recent N days?

Then it asks the same question again on the next day. That is why the average moves.

In this first Phase 3 lesson, we will not use rolling().mean(). We will build the calculation ourselves first.

1. Phase 3 Starts by Transforming Price

In Phase 2, we read price directly:

Open
High
Low
Close
→ candlesticks
→ pattern rules

Phase 3 begins a new step:

price data
→ calculation
→ indicator

A moving average is a good first indicator because the calculation is easy to see.

2. Start with Five Closing Prices

Imagine the last five Close prices were:

100
102
101
104
103

Add them:

100 + 102 + 101 + 104 + 103
= 510

Then divide by five:

510 / 5
= 102

So the 5-day Simple Moving Average is 102.

3. Why Is It Called “Moving”?

On the next trading day, the oldest price leaves the group and the newest price enters.

Day 1:
[100, 102, 101, 104, 103]
→ average

Day 2:
[102, 101, 104, 103, new price]
→ new average

The window moves forward one row at a time.

4. The Window Is a Parameter

We start with:

moving_average_days = 5

The number 5 is not built into the idea of a moving average. It is a parameter.

5-day average
10-day average
20-day average
50-day average
200-day average

The same calculation works with a different window size.

5. Build the Calculation as a Function

Our new lesson function is:

def simple_moving_average(
    values,
    window,
):
    ...

Its job is:

take the most recent window prices
→ add them
→ divide by window
→ save the average
→ move forward
→ repeat

6. Why the First Rows Are Empty

A 5-day moving average needs five prices.

On the first day, we have only one. On the second day, we have only two.

Day 1 → not enough prices
Day 2 → not enough prices
Day 3 → not enough prices
Day 4 → not enough prices
Day 5 → first 5-day average

So the first four moving-average values are None. That is not an error.

7. Complete Phase 3-1 Python Code

This lesson is short enough to use one complete file. You do not need a separate Core file.

Create:

phase3_01_moving_average_from_scratch.py

Copy the complete code below.

from pathlib import Path
from datetime import date, timedelta
import os

import FinanceDataReader as fdr
import matplotlib.pyplot as plt


# ============================================================
# Phase 3-1
# What Is a Moving Average?
# Build One from Prices with Python
# ============================================================


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

symbol = "AAPL"

recent_trading_days = 60

# Start with a short window so the calculation
# is easy to see and understand.
moving_average_days = 5


# ------------------------------------------------------------
# 2. LESSON FUNCTION
#    Build a Simple Moving Average from scratch.
# ------------------------------------------------------------

def simple_moving_average(
    values,
    window,
):
    """
    Calculate a Simple Moving Average without
    pandas rolling().mean().

    For each position, average the most recent
    'window' values.

    The first window - 1 positions return None
    because there are not enough prices yet.
    """

    if window <= 0:
        raise ValueError(
            "window must be greater than 0."
        )

    if len(values) < window:
        raise ValueError(
            "Not enough values for "
            "the selected moving-average window."
        )

    averages = [
        None
    ] * (
        window - 1
    )

    for i in range(
        window - 1,
        len(values),
    ):
        start = (
            i - window + 1
        )

        recent_values = (
            values[
                start:i + 1
            ]
        )

        average = (
            sum(recent_values)
            / window
        )

        averages.append(
            average
        )

    return averages


# ------------------------------------------------------------
# 3. Set the working folder
# ------------------------------------------------------------

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

os.chdir(
    SCRIPT_DIR
)


# ------------------------------------------------------------
# 4. Download recent market data
# ------------------------------------------------------------

end_date = (
    date.today()
    + timedelta(days=1)
)

start_date = (
    date.today()
    - timedelta(days=160)
)

df = fdr.DataReader(
    symbol,
    start_date.strftime(
        "%Y-%m-%d"
    ),
    end_date.strftime(
        "%Y-%m-%d"
    ),
)

if df.empty:
    raise ValueError(
        "No market data was returned."
    )

df = (
    df.tail(
        recent_trading_days
    )
    .copy()
)

print(
    "First available date:",
    df.index[0].strftime(
        "%Y-%m-%d"
    ),
)

print(
    "Most recent available date:",
    df.index[-1].strftime(
        "%Y-%m-%d"
    ),
)

print()


# ------------------------------------------------------------
# 5. Get Close prices as a Python list
# ------------------------------------------------------------

close_prices = (
    df["Close"]
    .astype(float)
    .tolist()
)


# ------------------------------------------------------------
# 6. Calculate the moving average from scratch
# ------------------------------------------------------------

moving_average_values = (
    simple_moving_average(
        values=close_prices,
        window=moving_average_days,
    )
)

df[
    f"SMA_{moving_average_days}"
] = moving_average_values


# ------------------------------------------------------------
# 7. Show one calculation by hand
# ------------------------------------------------------------

example_position = (
    moving_average_days - 1
)

example_start = (
    example_position
    - moving_average_days
    + 1
)

example_prices = (
    close_prices[
        example_start:
        example_position + 1
    ]
)

example_average = (
    sum(example_prices)
    / moving_average_days
)

print(
    f"First {moving_average_days}-day "
    "moving-average calculation:"
)

print()

for number, price in enumerate(
    example_prices,
    start=1,
):
    print(
        f"Price {number}:",
        f"{price:.2f}",
    )

print()

print(
    "Average:",
    f"{example_average:.2f}",
)

print()

print(
    "Stored SMA value:",
    f'{df.iloc[example_position][f"SMA_{moving_average_days}"]:.2f}',
)

print()


# ------------------------------------------------------------
# 8. Show the latest rows
# ------------------------------------------------------------

print(
    df[
        [
            "Close",
            f"SMA_{moving_average_days}",
        ]
    ]
    .tail(10)
)

print()


# ------------------------------------------------------------
# 9. Draw Close and the moving average
# ------------------------------------------------------------

figure_width = 11
figure_height = 6.5
figure_dpi = 100

fig, ax = plt.subplots(
    figsize=(
        figure_width,
        figure_height,
    ),
    dpi=figure_dpi,
)

ax.plot(
    df.index,
    df["Close"],
    label="Close",
    linewidth=1.8,
)

ax.plot(
    df.index,
    df[
        f"SMA_{moving_average_days}"
    ],
    label=(
        f"{moving_average_days}-day SMA"
    ),
    linewidth=2.2,
)

ax.set_title(
    f"{symbol} — Close and "
    f"{moving_average_days}-Day "
    "Simple Moving Average",
    fontsize=18,
)

ax.set_xlabel(
    "Date",
    fontsize=13,
)

ax.set_ylabel(
    "Price",
    fontsize=13,
)

ax.tick_params(
    axis="both",
    labelsize=10,
)

ax.grid(
    alpha=0.25,
)

ax.legend(
    fontsize=11,
)

fig.autofmt_xdate()

fig.subplots_adjust(
    left=0.10,
    right=0.97,
    top=0.90,
    bottom=0.18,
)

output_file = (
    SCRIPT_DIR
    / "moving_average_from_scratch.png"
)

fig.savefig(
    output_file,
    dpi=120,
    bbox_inches="tight",
)

print(
    "Saved:",
    output_file,
)

plt.show()
plt.close(fig)

8. Run the Program

python phase3_01_moving_average_from_scratch.py

The program will:

download recent AAPL data
→ get Close prices
→ calculate a 5-day average from scratch
→ print the first calculation
→ add the SMA column
→ plot Close and SMA
→ save the PNG

The chart is saved as:

moving_average_from_scratch.png

9. Why We Are Not Using rolling().mean() Yet

pandas can calculate the same idea much faster with a short command.

But if we start there, this:

rolling().mean()

can become a black box.

First understand:

recent prices
→ sum
→ divide
→ move the window
→ repeat

Later, a library function becomes a shortcut for something you already understand.

10. Change One Number

Run the program once with:

moving_average_days = 5

Then change only:

moving_average_days = 10

Run it again.

Look at the two lines and ask:

Which moving average is smoother?

Which one follows price more closely?

Which one reacts more slowly?

You should see the basic trade-off:

shorter window
→ reacts faster
→ less smooth

longer window
→ reacts slower
→ smoother

11. A Moving Average Does Not Predict by Itself

The moving average is made from prices that already happened.

It can help us summarize recent price behavior, but the line alone does not tell us that the next price must rise or fall.

price
→ moving average
→ smoother view of recent price

not

moving average
→ guaranteed future direction

What You Just Learned

raw Close prices
→ choose N
→ take the most recent N prices
→ calculate their average
→ move forward one row
→ repeat
→ moving-average line

You also saw an important Alphesta rule: understand the calculation before using the shortcut.

Where Do We Go Next?

One moving average is useful. Two moving averages make the idea of speed easier to see.

In Phase 3-2, we can compare a short moving average with a longer moving average on the same price series.