x <- c(100, 101, 102, 130, 103, 104, 105) # seven closes, one spike on day 4
print(x) # -> [1] 100 101 102 130 103 104 105[1] 100 101 102 130 103 104 105
A simple moving average adds up the last n closes and divides by n. Each of those closes counts the same, and the close that falls out of the back of the window stops counting altogether. An exponential moving average puts the most weight on today and fades the weight on older closes, without ever dropping one.
In this lesson I build both on seven prices I can check by hand, then a 50-day pair on AAA from the price file. Both are indicators: a number next to the price, nothing more.
Here are seven closes. Day 4 jumps to 130 and then the price comes straight back down.
[1] 100 101 102 130 103 104 105
rollmean() from zoo takes the mean of a sliding window. align = "right" puts the answer on the last day of the window, so day 3 holds the mean of days 1 to 3 and no future price ever leaks backwards. fill = NA pads the days before a full window exists, keeping the result the same length as the input.
[1] NA NA 101.0000 111.0000 111.6667 112.3333 104.0000
Two NA, because days 1 and 2 do not have three closes behind them. Day 3 is the first full window.
The exponential average has one number in it, the smoothing weight a = 2 / (span + 1). Today’s average is a times today’s price plus 1 - a times yesterday’s average. Yesterday’s average already contains the day before, which already contains the day before that, so every past close is still in there with a smaller and smaller share.
I write it as a loop so the recursion is on the page.
ema_of <- function(price, span) {
a <- 2 / (span + 1) # weight on today's price
out <- numeric(length(price)) # room for one answer per day
out[1] <- price[1] # seed with the first price
for (i in 2:length(price)) { # walk forward one day at a time
out[i] <- a * price[i] + (1 - a) * out[i - 1] # today's price, then yesterday's average
}
out # hand back the whole column
}
print(2 / (3 + 1)) # -> [1] 0.5 <- a for span 3[1] 0.5
A span of 3 gives a = 0.5, so each day is half today’s price and half everything before it.
[1] 100.0000 100.5000 101.2500 115.6250 109.3125 106.6562 105.8281
Day 4 by hand: half of the spike, plus half of day 3’s average of 101.25.
[1] 115.625
[1] 115.625
[1] TRUE
The weights on today, yesterday, the day before, and so on, are a, a(1-a), a(1-a)^2 and onwards. They shrink but never reach zero.
Put the two side by side.
price sma3 ema3
1 100 NA 100.00
2 101 NA 100.50
3 102 101.00 101.25
4 130 111.00 115.62
5 103 111.67 109.31
6 104 112.33 106.66
7 105 104.00 105.83
Days 4, 5 and 6 all contain the spike, so the simple average sits above 111 for three days running. On day 7 the spike falls out of the window and the average drops back to 104 in one step. The exponential average takes the spike hardest on day 4, at 115.62, then works it off a bit at a time.
[1] 8.3333
[1] 0.8281
Nothing happened to the price on day 7: it went from 104 to 105. The simple average moved 8.33 anyway, because a number three days old left the calculation. The exponential average moved 0.83, since it never drops anything, it only shrinks it.
prices.csv sits next to this lesson and holds simulated daily closes for four tickers over six years. read.csv() returns Date as character, so as.Date() makes it a real date, which sorting and later joins need.
library(dplyr) # filter, arrange, mutate
px <- read.csv("prices.csv") # simulated closes, 6264 rows
px$Date <- as.Date(px$Date) # character to Date
aapl <- px |>
filter(Ticker == "AAA") |> # one ticker
arrange(Date) |> # oldest first, both averages need it
mutate(sma50 = rollmean(Close, 50, fill = NA, align = "right"), # last 50 closes, equal weight
ema50 = ema_of(Close, 50)) # span 50, weight 2/51 on today
print(nrow(aapl)) # -> [1] 1566[1] 1566
Sorting first is not optional. Both averages read the rows in the order they arrive, so an unsorted table gives an average of nothing in particular.
Date Close sma50 ema50
1562 2025-12-25 193.74 195.02 192.37
1563 2025-12-26 197.62 195.16 192.58
1564 2025-12-29 196.94 195.27 192.75
1565 2025-12-30 196.56 195.34 192.90
1566 2025-12-31 199.30 195.50 193.15
Both track the price, and they differ by a couple of points at the end of the sample. Across the days where both exist they sit 1.19 apart on average and never more than 5.50 apart.
[1] 1.19
[1] 5.5
The two differ at the start of the sample as well. Count the missing values.
[1] 49
[1] 0
49 and 0. The simple average refuses to answer until it has 50 closes, so the column starts on row 50. The loop seeds itself with the first close and answers from row 1.
Date Close sma50 ema50
48 2020-03-06 78.65 NA 76.33
49 2020-03-09 78.50 NA 76.42
50 2020-03-10 80.98 76.20 76.60
51 2020-03-11 79.65 76.29 76.72
52 2020-03-12 78.64 76.32 76.79
That zero is not free. The first ema50 is the first close exactly, and the next few are close to it, because with a = 2/51 each new day moves the average by about 4% of the distance to the price.
[1] 75.0800 75.1529 75.2387
[1] 75.08 76.94 77.34
Day 1 of the exponential column is a copy of the price, not an average of fifty days. If you need the two columns to answer on the same footing, drop the first 50 rows of both.
Build a 20-day simple and a 20-day exponential average of CCC’s close from prices.csv, print the last four rows, and count the NA in each column.
library(dplyr)
library(zoo)
px <- read.csv("prices.csv")
px$Date <- as.Date(px$Date)
msft <- px |>
filter(Ticker == "CCC") |> # one ticker
arrange(Date) |> # oldest first
mutate(sma20 = round(rollmean(Close, 20, fill = NA, align = "right"), 2), # last 20 closes
ema20 = round(ema_of(Close, 20), 2)) # span 20, a = 2/21
print(tail(msft[, c("Date", "Close", "sma20", "ema20")], 4))
# -> Date Close sma20 ema20
# -> 1563 2025-12-26 792.88 794.61 794.22
# -> 1564 2025-12-29 817.75 794.42 796.46
# -> 1565 2025-12-30 826.31 794.59 799.30
# -> 1566 2025-12-31 828.51 795.60 802.08
print(sum(is.na(msft$sma20))) # -> [1] 19
print(sum(is.na(msft$ema20))) # -> [1] 0Four rising closes at the end of the sample. The exponential average climbs from 794.22 to 802.08 over those four days while the simple one moves less than a point, because each new close carries 2/21 of the exponential average and only 1/20 of the simple one, against 19 older closes that have not changed.
Lesson 22 builds a second indicator, the relative strength index, from the same closes.