Through this project, I attempted to test the feasibility of using the 5-day and 20-day moving averages to generate trading signals. There are two common types of moving averages, which are simple and exponential (typically shortened to SMA and EMA) and can be implemented on different timeframes, from the 1-minute stock prices to the daily or lower period values. I used the SMA and EMA on the daily timeframe to generate signals for short-term trading and tested its effectiveness.
The results show that SMA and EMA can provide excess returns for some stocks and may lead to poor returns when used on other stocks. However, these results were based on the assumptions of zero trading costs and that trades could be filled at the opening prices at any point in time. By including trading costs, one would obtain significantly lower returns depending on how frequent buy and sell signals were generated. Furthermore, past performance is not indicative of future performance and thus, if the results show that the moving average strategy worked for a stock based on the backtest, it may not work in the future.
library(tidyquant) # Loads quantmod, PerformanceAnalytics and TTR packages
library(tidyverse) # Loads dplyr and ggplot2 packages (data manipulation and plotting)
I implemented the trading strategies on Apple Inc. (AAPL), Exxon Mobil Corporation (XOM), Microsoft Corporation (MSFT), Procter & Gamble Company (PG) and Merck & Co. Inc. (MRK). I first tested the strategies using SMAs, followed by EMAs, and compared the performance to buying and holding the ETFs over the period from January 2018 to October 2022.
While there are many strategies involving moving averages (MAs), I used a simple rule to determine the buy or sell decision. The steps to this trading strategy are:
I would buy or sell the stocks at the opening price the next trading day after the MAs provide a signal. I assumed zero trading costs and that all trades can be filled at any point in time, which is not practical, but allowed me to test the effectiveness of the strategies in a simple manner.
The opening and closing prices of the stocks can be obtained using
quantmod::getSymbols()
.
# Set start and end date for data retrieval
<- as.Date("2018-01-01")
startdate <- as.Date("2022-11-01")
enddate
# Create vector of tickers
<- c("AAPL", "XOM", "MSFT", "PG", "MRK")
ticker
<- list()
dat
for (t in seq(ticker)) {
<- quantmod::getSymbols(Symbols = ticker[t], src = "yahoo", auto.assign = FALSE,
dat[[ticker[t]]] from = startdate, to = enddate, periodicity = "daily")
}
names(dat); lapply(dat, head, 3)
## [1] "AAPL" "XOM" "MSFT" "PG" "MRK"
## $AAPL
## AAPL.Open AAPL.High AAPL.Low AAPL.Close AAPL.Volume AAPL.Adjusted
## 2018-01-02 42.5400 43.0750 42.315 43.0650 102223600 40.95049
## 2018-01-03 43.1325 43.6375 42.990 43.0575 118071600 40.94337
## 2018-01-04 43.1350 43.3675 43.020 43.2575 89738400 41.13356
##
## $XOM
## XOM.Open XOM.High XOM.Low XOM.Close XOM.Volume XOM.Adjusted
## 2018-01-02 83.82 85.20 83.66 85.03 11469300 65.37909
## 2018-01-03 85.16 86.97 84.82 86.70 13957700 66.66314
## 2018-01-04 86.79 87.22 86.43 86.82 10863000 66.75540
##
## $MSFT
## MSFT.Open MSFT.High MSFT.Low MSFT.Close MSFT.Volume MSFT.Adjusted
## 2018-01-02 86.13 86.31 85.50 85.95 22483800 80.94038
## 2018-01-03 86.06 86.51 85.97 86.35 26061400 81.31709
## 2018-01-04 86.59 87.66 86.57 87.11 21912000 82.03278
##
## $PG
## PG.Open PG.High PG.Low PG.Close PG.Volume PG.Adjusted
## 2018-01-02 91.92 91.93 90.55 90.65 7558900 79.11253
## 2018-01-03 90.73 91.09 90.52 90.54 5863600 79.01653
## 2018-01-04 90.83 91.77 90.61 91.18 6322500 79.57506
##
## $MRK
## MRK.Open MRK.High MRK.Low MRK.Close MRK.Volume MRK.Adjusted
## 2018-01-02 53.99809 54.15076 53.48282 53.64504 10556504 46.21742
## 2018-01-03 53.65458 53.68321 53.39695 53.56870 11090460 46.15166
## 2018-01-04 53.85496 54.79008 53.79771 54.43702 15751650 46.89975
The 5-day and 20-day SMAs can be calculated using
TTR::SMA()
.
for (i in seq(dat)) {
$SMA5 <- TTR::SMA(x = quantmod::Cl(dat[[i]]), n = 5)
dat[[i]]
$SMA20 <- TTR::SMA(x = quantmod::Cl(dat[[i]]), n = 20)
dat[[i]] }
Generate trading signals by checking if SMA5 is above SMA20 or if the SMA20 is on an uptrend:
# Generate TRUE/FALSE boolean, which is converted to 1 or 0
for (i in seq(dat)) {
$SMAsignal <- dat[[i]]$SMA5 > dat[[i]]$SMA20 | diff(dat[[i]]$SMA20, lag = 5)/5 > 0
dat[[i]] }
Since I buy or sell at the opening price, I would calculate returns based on these prices for simplicity.
# Calculate returns using opening price and lag the SMAsignal twice for backtesting
# Reason: buy the next day after the SMAsignal and returns are calculated using previous day value
for (i in seq(dat)) {
$SMAreturn <- Return.calculate(prices = Op(dat[[i]]), method = "log") * Lag(dat[[i]]$SMAsignal, k = 2)
dat[[i]] }
I plotted the returns from trading each stock against the returns earned by holding the stock over the period.
# Calculate the returns earned by holding the stocks using adjusted prices
# Adjusted prices include splits and dividends information
for (i in seq(dat)) {
$total.return <- Return.calculate(prices = Ad(dat[[i]]), method = "log")
dat[[i]]
}
# Save the cumulative return data in a separate dataframe
<- NULL
SMAreturn_plot
for (i in seq(dat)) {
<- cbind(SMAreturn_plot,
SMAreturn_plot 10000 * exp(cumsum(na.omit(dat[[i]]$SMAreturn))),
10000 * exp(cumsum(na.omit(dat[[i]]$total.return))))
}
colnames(SMAreturn_plot) <- paste(rep(ticker, each = 2),
rep(c("SMA Trading Strategy", "Total Return"), length(dat)))
for (i in seq(from = 1, to = ncol(SMAreturn_plot), by = 2)) {
plot(SMAreturn_plot[, c(i, i+1)],
main = paste("Comparison of Cumulative Returns for", rep(ticker, each = 2)[i]),
legend.loc = "topleft", grid.col = NA) %>%
print()
}
Calculate the annualized returns and standard deviation for
comparison of performance using
PerformanceAnalytics::table.AnnualizedReturns()
.
for (i in seq(dat)) {
::table.AnnualizedReturns(R = dat[[i]][, c("SMAreturn","total.return")],
PerformanceAnalyticsscale = 252,
Rf = 0.03/252,
geometric = FALSE) %>%
`colnames<-`(paste(rep(ticker[i], 2),
rep(c("SMA Trading Strategy", "Total Return")))) %>%
print()
}
## AAPL SMA Trading Strategy AAPL Total Return
## Annualized Return 0.3190 0.2733
## Annualized Std Dev 0.2416 0.3330
## Annualized Sharpe (Rf=3%) 1.1963 0.7306
## XOM SMA Trading Strategy XOM Total Return
## Annualized Return 0.1819 0.1077
## Annualized Std Dev 0.2660 0.3410
## Annualized Sharpe (Rf=3%) 0.5711 0.2278
## MSFT SMA Trading Strategy MSFT Total Return
## Annualized Return 0.0627 0.2178
## Annualized Std Dev 0.2213 0.3087
## Annualized Sharpe (Rf=3%) 0.1477 0.6082
## PG SMA Trading Strategy PG Total Return
## Annualized Return 0.0954 0.1102
## Annualized Std Dev 0.1347 0.2201
## Annualized Sharpe (Rf=3%) 0.4854 0.3645
## MRK SMA Trading Strategy MRK Total Return
## Annualized Return -0.0283 0.1624
## Annualized Std Dev 0.1766 0.2341
## Annualized Sharpe (Rf=3%) -0.3305 0.5657
The SMA trading strategy did not work for all 5 stocks. It provided excess returns when implemented on AAPL and XOM, but produced subpar results for MSFT, PG and MRK. For AAPL and XOM, the excess returns may be diminished significantly if trading costs are included.
Instead of using SMA5 and SMA20, I used the EMA5 and EMA20 to generate trading signals. All the steps involved are the same as in Section 3.
The 5-day and 20-day EMAs can be calculated using
TTR::EMA()
.
for (i in seq(dat)) {
$EMA5 <- TTR::EMA(x = quantmod::Cl(dat[[i]]), n = 5)
dat[[i]]
$EMA20 <- TTR::EMA(x = quantmod::Cl(dat[[i]]), n = 20)
dat[[i]] }
Generate trading signals by checking if EMA5 is above EMA20 or if the EMA20 is on an uptrend:
# Generate TRUE/FALSE boolean, which is converted to 1 or 0
for (i in seq(dat)) {
$EMAsignal <- dat[[i]]$EMA5 > dat[[i]]$EMA20 | diff(dat[[i]]$EMA20, lag = 5)/5 > 0
dat[[i]] }
Calculate returns from EMA trading strategy:
# Calculate returns using opening price and lag the EMAsignal twice for backtesting
# Reason: buy the next day after the EMAsignal and returns are calculated using previous day value
for (i in seq(dat)) {
$EMAreturn <- Return.calculate(prices = Op(dat[[i]]), method = "log") * Lag(dat[[i]]$EMAsignal, k = 2)
dat[[i]] }
Plot the returns from trading each stock against the returns earned by holding the stock over the period:
# Save the cumulative return data in a separate dataframe
<- NULL
EMAreturn_plot
for (i in seq(dat)) {
<- cbind(EMAreturn_plot,
EMAreturn_plot 10000 * exp(cumsum(na.omit(dat[[i]]$EMAreturn))),
10000 * exp(cumsum(na.omit(dat[[i]]$total.return))))
}
colnames(EMAreturn_plot) <- paste(rep(ticker, each = 2),
rep(c("EMA Trading Strategy", "Total Return"), length(dat)))
for (i in seq(from = 1, to = ncol(EMAreturn_plot), by = 2)) {
plot(EMAreturn_plot[, c(i, i+1)],
main = paste("Comparison of Cumulative Returns for", rep(ticker, each = 2)[i]),
legend.loc = "topleft", grid.col = NA) %>%
print()
}
Calculate the annualized returns and standard deviation for
comparison of performance using
PerformanceAnalytics::table.AnnualizedReturns()
.
for (i in seq(dat)) {
::table.AnnualizedReturns(R = dat[[i]][, c("EMAreturn","total.return")],
PerformanceAnalyticsscale = 252,
Rf = 0.03/252,
geometric = FALSE) %>%
`colnames<-`(paste(rep(ticker[i], 2),
rep(c("EMA Trading Strategy", "Total Return")))) %>%
print()
}
## AAPL EMA Trading Strategy AAPL Total Return
## Annualized Return 0.3251 0.2733
## Annualized Std Dev 0.2230 0.3330
## Annualized Sharpe (Rf=3%) 1.3234 0.7306
## XOM EMA Trading Strategy XOM Total Return
## Annualized Return 0.1178 0.1077
## Annualized Std Dev 0.2478 0.3410
## Annualized Sharpe (Rf=3%) 0.3544 0.2278
## MSFT EMA Trading Strategy MSFT Total Return
## Annualized Return 0.0657 0.2178
## Annualized Std Dev 0.2040 0.3087
## Annualized Sharpe (Rf=3%) 0.1748 0.6082
## PG EMA Trading Strategy PG Total Return
## Annualized Return 0.0991 0.1102
## Annualized Std Dev 0.1293 0.2201
## Annualized Sharpe (Rf=3%) 0.5345 0.3645
## MRK EMA Trading Strategy MRK Total Return
## Annualized Return -0.0059 0.1624
## Annualized Std Dev 0.1645 0.2341
## Annualized Sharpe (Rf=3%) -0.2183 0.5657
While the EMA trading strategy improved the returns from trading AAPL and PG, it reduced the returns from trading XOM significantly. This shows that the SMA and EMA strategy does not work for all stocks and cannot be implemented blindly.
Instead of using EMA and SMA separately, this section attempted to implemented both to generate trading signals. I used the EMA5 and SMA20, such that the slower MA is not too reactive compared to the faster MA.
Since we already have the EMA5 and SMA20 from the previous sections, I only need to generate the trading signals and calculate the returns.
# Generate TRUE/FALSE boolean, which is converted to 1 or 0
for (i in seq(dat)) {
$EMA_SMA.signal <- dat[[i]]$EMA5 > dat[[i]]$SMA20 | diff(dat[[i]]$SMA20, lag = 5)/5 > 0
dat[[i]] }
Calculate returns from EMA trading strategy:
# Calculate returns using opening price and lag the EMAsignal twice for backtesting
# Reason: buy the next day after the EMAsignal and returns are calculated using previous day value
for (i in seq(dat)) {
$EMA_SMA.return <- Return.calculate(prices = Op(dat[[i]]), method = "log") * Lag(dat[[i]]$EMA_SMA.signal, k = 2)
dat[[i]] }
Plot the returns from trading each stock against the returns earned by holding the stock over the period:
# Save the cumulative return data in a separate dataframe
<- NULL
EMASMA_plot
for (i in seq(dat)) {
<- cbind(EMASMA_plot,
EMASMA_plot 10000 * exp(cumsum(na.omit(dat[[i]]$EMA_SMA.return))),
10000 * exp(cumsum(na.omit(dat[[i]]$total.return))))
}
colnames(EMASMA_plot) <- paste(rep(ticker, each = 2),
rep(c("EMA+SMA Trading Strategy", "Total Return"), length(dat)))
for (i in seq(from = 1, to = ncol(EMASMA_plot), by = 2)) {
plot(EMASMA_plot[, c(i, i+1)],
main = paste("Comparison of Cumulative Returns for", rep(ticker, each = 2)[i]),
legend.loc = "topleft", grid.col = NA) %>%
print()
}
Calculate the annualized returns and standard deviation for
comparison of performance using
PerformanceAnalytics::table.AnnualizedReturns()
.
for (i in seq(dat)) {
::table.AnnualizedReturns(R = dat[[i]][, c("EMA_SMA.return","total.return")],
PerformanceAnalyticsscale = 252,
Rf = 0.03/252,
geometric = FALSE) %>%
`colnames<-`(paste(rep(ticker[i], 2),
rep(c("EMA+SMA Trading Strategy", "Total Return")))) %>%
print()
}
## AAPL EMA+SMA Trading Strategy AAPL Total Return
## Annualized Return 0.3183 0.2733
## Annualized Std Dev 0.2427 0.3330
## Annualized Sharpe (Rf=3%) 1.1882 0.7306
## XOM EMA+SMA Trading Strategy XOM Total Return
## Annualized Return 0.1795 0.1077
## Annualized Std Dev 0.2664 0.3410
## Annualized Sharpe (Rf=3%) 0.5613 0.2278
## MSFT EMA+SMA Trading Strategy MSFT Total Return
## Annualized Return 0.0378 0.2178
## Annualized Std Dev 0.2232 0.3087
## Annualized Sharpe (Rf=3%) 0.0351 0.6082
## PG EMA+SMA Trading Strategy PG Total Return
## Annualized Return 0.0887 0.1102
## Annualized Std Dev 0.1349 0.2201
## Annualized Sharpe (Rf=3%) 0.4349 0.3645
## MRK EMA+SMA Trading Strategy MRK Total Return
## Annualized Return 0.0208 0.1624
## Annualized Std Dev 0.1781 0.2341
## Annualized Sharpe (Rf=3%) -0.0514 0.5657
The combination of EMA and SMA generated excess returns for AAPL and XOM, while creating slight positive annualized return for MRK. As mentioned, the inclusion of trading costs would significantly reduce returns from trading, and is not a long-term solution as compared to holding great stocks.