In this project, I backtested the Moving Average Convergence/Divergence (MACD) trading strategy. The MACD indicator requires 4 main inputs, which are the periods for the fast and slow moving averages, the period for the signal moving average and the type of moving averages to use. The inputs would mostly depend on the trader’s trading horizon (e.g. intraday, scalping, swing trading etc.). For me, I prefer to use the exponential moving averages with the 20-period, 50-period and 5-period as my fast, slow and signal EMAs.
The results in this project were based on the assumptions of zero trading costs and that trades could be filled at the opening prices at any point in time. Therefore, any excess return that was achieved through the MACD trading strategy may not be a good proxy of actual performance. Furthermore, past performance is not indicative of future performance.
library(tidyquant) # Loads quantmod, PerformanceAnalytics and TTR packages
library(tidyverse) # Loads dplyr and ggplot2 packages (data manipulation and plotting)
In my previous project Backtesting a 5-day and 20-day Moving Average Trading Strategy, I implemented a moving average trading strategy on Apple Inc. (AAPL), Exxon Mobil Corporation (XOM), Microsoft Corporation (MSFT), Procter & Gamble Company (PG) and Merck & Co. Inc. (MRK). I would use these same stocks, with price data from January 2018 to October 2022, for my MACD trading strategy to allow for some comparison of trading results.
The steps for the trading strategy are:
To determine if the lines are upward sloping, I would find the difference of the current MACD or signal value from the value 5 periods ago. I would buy or sell the stocks at the opening price the next trading day after the trading signal is established. 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 strategy in a simple manner.
The historical stock prices 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")
}
lapply(dat, head, 3)
## $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.94336
## 2018-01-04 43.1350 43.3675 43.020 43.2575 89738400 41.13354
##
## $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.37910
## 2018-01-03 85.16 86.97 84.82 86.70 13957700 66.66312
## 2018-01-04 86.79 87.22 86.43 86.82 10863000 66.75541
##
## $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.94041
## 2018-01-03 86.06 86.51 85.97 86.35 26061400 81.31708
## 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.57509
##
## $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.21743
## 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.89977
The MACD and signal values can be calculated using
TTR::MACD()
. I also added a time component
and included the regression to find the slope of the MACD and signal
lines.
# Create MACD and signal values, time component and regress to find slops
for (i in seq(dat)) {
<- cbind(dat[[i]], TTR::MACD(x = quantmod::Cl(dat[[i]]),
dat[[i]] nFast = 20,
nSlow = 50,
nSig = 5,
maType = "EMA"))
$MACDslope <- diff(dat[[i]]$macd, lag = 5)
dat[[i]]
$SIGslope <- diff(dat[[i]]$signal, lag = 5)
dat[[i]] }
Generate trading signals by checking if MACD is above the signal line and if either MACD or the signal line is upward sloping:
# Remove rows with missing data
<- lapply(dat, na.omit)
dat2
# Use for loops to determine if conditions were met
for (i in seq(dat2)) {
$trade.signal <- (dat2[[i]]$macd > dat2[[i]]$signal) & (dat2[[i]]$MACDslope > 0) | (dat2[[i]]$SIGslope > 0)
dat2[[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 trade.signal twice for backtesting
# Reason: buy the next day after the trade.signal and returns are calculated using previous day value
for (i in seq(dat2)) {
$MACDreturn <- Return.calculate(prices = Op(dat2[[i]]), method = "log") * Lag(dat2[[i]]$trade.signal, k = 2)
dat2[[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(dat2)) {
$total.return <- Return.calculate(prices = Ad(dat2[[i]]), method = "log")
dat2[[i]]
}
# Save the cumulative return data in a separate dataframe
<- NULL
return.plot
for (i in seq(dat2)) {
<- cbind(return.plot,
return.plot 10000 * exp(cumsum(na.omit(dat2[[i]]$MACDreturn))),
10000 * exp(cumsum(na.omit(dat2[[i]]$total.return))))
}
colnames(return.plot) <- paste(rep(ticker, each = 2),
rep(c("MACD Trading Strategy", "Total Return"), length(dat2)))
for (i in seq(from = 1, to = ncol(return.plot), by = 2)) {
plot(return.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(dat2)) {
::table.AnnualizedReturns(R = dat2[[i]][, c("MACDreturn","total.return")],
PerformanceAnalyticsscale = 252,
Rf = 0.03/252,
geometric = FALSE) %>%
`colnames<-`(paste(rep(ticker[i], 2),
rep(c("MACD Trading Strategy", "Total Return")))) %>%
print()
}
## AAPL MACD Trading Strategy AAPL Total Return
## Annualized Return 0.3211 0.2911
## Annualized Std Dev 0.2007 0.3358
## Annualized Sharpe (Rf=3%) 1.4505 0.7775
## XOM MACD Trading Strategy XOM Total Return
## Annualized Return 0.1882 0.1420
## Annualized Std Dev 0.2600 0.3452
## Annualized Sharpe (Rf=3%) 0.6084 0.3244
## MSFT MACD Trading Strategy MSFT Total Return
## Annualized Return 0.0416 0.2189
## Annualized Std Dev 0.1951 0.3079
## Annualized Sharpe (Rf=3%) 0.0592 0.6135
## PG MACD Trading Strategy PG Total Return
## Annualized Return 0.0543 0.1474
## Annualized Std Dev 0.1221 0.2216
## Annualized Sharpe (Rf=3%) 0.1989 0.5298
## MRK MACD Trading Strategy MRK Total Return
## Annualized Return 0.0987 0.1784
## Annualized Std Dev 0.1718 0.2345
## Annualized Sharpe (Rf=3%) 0.4002 0.6331
Similar to the moving average trading strategy I tested previously, the MACD trading strategy worked for some stocks while performing poorly for others. This again showed that a trading strategy does not work equally for all stocks. Despite achieving excess returns for AAPL and XOM, it was obtained under the assumption of zero transaction costs. It would be safer to assume that the transaction costs would significantly reduce the returns from active trading, depending on the frequency of the trades made.