What is the difference between an SMA and an EMA?

Compute a simple moving average with zoo::rollmean and an exponential one as a short loop, and see how each reacts when a spike leaves the window.

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.

Step 1. Seven prices with one spike

Here are seven closes. Day 4 jumps to 130 and then the price comes straight back down.

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

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.

library(zoo)                                        # rollmean

sma <- rollmean(x, 3, fill = NA, align = "right")   # mean of the last three closes

print(round(sma, 4))
[1]       NA       NA 101.0000 111.0000 111.6667 112.3333 104.0000
# -> [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.

print(mean(x[2:4]))   # -> [1] 111   <- days 2, 3 and 4 by hand
[1] 111
print(sma[4])         # -> [1] 111
[1] 111

Step 2. The exponential average as a loop

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.

ema <- ema_of(x, 3)     # exponential average of the same seven closes

print(round(ema, 4))
[1] 100.0000 100.5000 101.2500 115.6250 109.3125 106.6562 105.8281
# -> [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.

print(0.5 * 130 + 0.5 * 101.25)                      # -> [1] 115.625   <- by hand
[1] 115.625
print(ema[4])                                        # -> [1] 115.625
[1] 115.625
print(all.equal(ema[4], 0.5 * 130 + 0.5 * ema[3]))   # -> [1] TRUE
[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.

a <- 2 / (3 + 1)          # the same smoothing weight
w <- a * (1 - a)^(0:4)    # weight on today and the four days before it

print(round(w, 4))        # -> [1] 0.5000 0.2500 0.1250 0.0625 0.0312
[1] 0.5000 0.2500 0.1250 0.0625 0.0312

Step 3. What happens when the spike leaves

Put the two side by side.

both <- data.frame(price = x,                  # the closes
                   sma3  = round(sma, 2),      # simple, three days
                   ema3  = round(ema, 2))      # exponential, span 3

print(both)
  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
# ->   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.

print(round(sma[6] - sma[7], 4))   # -> [1] 8.3333   <- simple, day 6 to day 7
[1] 8.3333
print(round(ema[6] - ema[7], 4))   # -> [1] 0.8281   <- exponential, same two days
[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.

Step 4. Fifty days of AAA

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.

avgs <- aapl |>
  mutate(sma50 = round(sma50, 2),      # two decimals for printing
         ema50 = round(ema50, 2)) |>
  select(Date, Close, sma50, ema50)    # the four columns worth looking at

print(tail(avgs, 5))
           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
# ->            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.

gap <- aapl |> filter(!is.na(sma50))                    # days where both averages exist

print(round(mean(abs(gap$sma50 - gap$ema50)), 2))       # -> [1] 1.19
[1] 1.19
print(round(max(abs(gap$sma50 - gap$ema50)), 2))        # -> [1] 5.5
[1] 5.5

The two differ at the start of the sample as well. Count the missing values.

print(sum(is.na(aapl$sma50)))    # -> [1] 49   <- days without 50 closes behind them
[1] 49
print(sum(is.na(aapl$ema50)))    # -> [1] 0    <- the loop seeds on day 1
[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.

print(avgs[48:52, ])
         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
# ->          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.

print(round(head(aapl$ema50, 3), 4))   # -> [1] 75.0800 75.1529 75.2387
[1] 75.0800 75.1529 75.2387
print(head(aapl$Close, 3))             # -> [1] 75.08 76.94 77.34
[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.

Your turn

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] 0

Four 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.