The purpose of this file is to show briefly how you could apply moving average concept in the stock market. I will be using simple moving average to define the trading signal.

One of the trading strategy using the technical analysis is that if the short-term moving average crosses the long-term moving average, people consider that as a bullish signal or a Golden Cross. On the other hand, bearish signal or Dead Cross is the opposite concept where the long-term moving average crosses the short-term moving average.

In this exercise, I will be using 50 day simple moving average for a short-term and 200 day moving average for a long-term moving average. Also, I will buy the stock at the Golden Cross signal and sell it at Dead Cross signal.

Some people use shorter moving average or weighted moving average to capture the signal, but I will be only using simple moving average in this exercise. The stock I will be using is QQQ which is an ETF that tracks NASDAQ index.

# Import library
library(quantmod)
## Loading required package: xts
## Loading required package: zoo
## 
## Attaching package: 'zoo'
## The following objects are masked from 'package:base':
## 
##     as.Date, as.Date.numeric
## Loading required package: TTR
## Registered S3 method overwritten by 'quantmod':
##   method            from
##   as.zoo.data.frame zoo
library(magrittr)
# Import QQQ
getSymbols(Symbols = 'QQQ',from='2016-01-01')
## [1] "QQQ"
# Extract adjusted price
qqq <- QQQ[,6]
head(qqq)
##            QQQ.Adjusted
## 2016-01-04    103.23029
## 2016-01-05    103.05119
## 2016-01-06    102.06129
## 2016-01-07     98.86542
## 2016-01-08     98.05466
## 2016-01-11     98.35633
# 50 day moving average
sma50 <- SMA(qqq,n=50)
tail(sma50)
##                 SMA
## 2023-10-11 366.9162
## 2023-10-12 366.8378
## 2023-10-13 366.6784
## 2023-10-16 366.6366
## 2023-10-17 366.5076
## 2023-10-18 366.3458
# 200 day moving average
sma200 <- SMA(qqq,n=200) 
tail(sma200)
##                 SMA
## 2023-10-11 332.9698
## 2023-10-12 333.4885
## 2023-10-13 334.0027
## 2023-10-16 334.5549
## 2023-10-17 335.0695
## 2023-10-18 335.5608
library(timetk)

# Merge the dataset
df <- merge(qqq,sma50,sma200)

# Convert into tibble format
data <- df %>% tk_tbl()

# Change the column names
colnames(data) <- c('Date','QQQ','SMA50','SMA200')
tail(data)
## # A tibble: 6 x 4
##   Date         QQQ SMA50 SMA200
##   <date>     <dbl> <dbl>  <dbl>
## 1 2023-10-11  371.  367.   333.
## 2 2023-10-12  370.  367.   333.
## 3 2023-10-13  365.  367.   334.
## 4 2023-10-16  369.  367.   335.
## 5 2023-10-17  368.  367.   335.
## 6 2023-10-18  363.  366.   336.

We have created a dataframe that contains date, qqq, sma50, sma 200. Now, we have to create a trading signal; since we have to find a point where SMA 50 crosses SMA200, SMA50 should be bigger than SMA200 but the one-period lagged SMA50 should be smaller than the one-period lagged SMA200. This will be clearer if you look at the visualization below.

library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:xts':
## 
##     first, last
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
# Creating trading signal
data$signal <- ifelse(data$SMA50 > data$SMA200 & lag(data$SMA50) <= lag(data$SMA200), 1,
                      ifelse(data$SMA50 < data$SMA200 & lag(data$SMA50) >= lag(data$SMA200), -1, 0))

tail(data)
## # A tibble: 6 x 5
##   Date         QQQ SMA50 SMA200 signal
##   <date>     <dbl> <dbl>  <dbl>  <dbl>
## 1 2023-10-11  371.  367.   333.      0
## 2 2023-10-12  370.  367.   333.      0
## 3 2023-10-13  365.  367.   334.      0
## 4 2023-10-16  369.  367.   335.      0
## 5 2023-10-17  368.  367.   335.      0
## 6 2023-10-18  363.  366.   336.      0
library(DT)

# See where the signal is made
long_signal_table <- data[data$signal==1,] %>% na.omit()
short_signal_table <- data[data$signal==-1,] %>% na.omit()
short_signal_table <- short_signal_table[-1,]

long_signal_table %>% datatable()
short_signal_table %>% datatable()
library(ggplot2)

data %>%
  ggplot(aes(x=Date,y=QQQ,color='QQQ'))+
  geom_line(size=0.7)+
  geom_line(aes(y=SMA50,color='SMA 50'),size=1,linetype = "longdash")+
  geom_line(aes(y=SMA200,color='SMA 200'),size=1,linetype = "longdash")+
  geom_point(data=long_signal_table, aes(x=Date, y=QQQ,color='Buy'),size=4) +
  geom_point(data=short_signal_table, aes(x=Date, y=QQQ,color='Sell'),size=4) +
  scale_color_manual(name="Color", values=c("QQQ"="black","SMA 50"="steelblue", "SMA 200" = "gold2","Buy" = "green4","Sell"='red3')) +
  theme_minimal()+
  theme(legend.position = "top") +
  scale_x_date(date_breaks = '6 months',
               date_labels = '%Y-%m')+
  labs(title='QQQ Golden Cross Trading Signal')
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## i Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Removed 49 rows containing missing values (`geom_line()`).
## Warning: Removed 199 rows containing missing values (`geom_line()`).

The black line shows adjusted price of QQQ, the dashed line represent moving average, and the points tell when to buy and sell the stock. Since 2016, we had 3 buying signals if we used this strategy. Assuming we buy at green point and sell it at the red point, let’s compare the return using this strategy with buy&hold. We will compare the performance starting from 2019-04-01 when the first signal appeared

# Calculating returns
initial_price <- long_signal_table$QQQ[1]
end_price <- data[nrow(data),]$QQQ
hold_ret <- (end_price/initial_price)-1
strategy_returns <- rep(0,(nrow(long_signal_table)))

# automate returns until the last selling signal
for (i in 1:nrow(short_signal_table)) {
  strategy_returns[i] <- short_signal_table[i, "QQQ"]/long_signal_table[i, "QQQ"] - 1
}

# assume that we sell the stock now since there is a buy signal but no sell signal yet
strategy_returns[nrow(long_signal_table)] <- end_price/long_signal_table[nrow(long_signal_table),'QQQ']-1

strategy_returns <- strategy_returns %>% as.numeric()
strategy_returns
## [1] 0.1786406 0.5433879 0.2648043
# calculate total strategy returns
total_strategy_ret <- 1
for (i in 1:length(strategy_returns))
  {
  total_strategy_ret <- total_strategy_ret*(1+strategy_returns[i])
}

total_strategy_ret <- total_strategy_ret-1
total_strategy_ret
## [1] 1.300805
# Create a data frame for plotting
ret_data <- data.frame(Trade = 1:length(strategy_returns), Returns = as.numeric(strategy_returns))
ret_data
##   Trade   Returns
## 1     1 0.1786406
## 2     2 0.5433879
## 3     3 0.2648043
library(viridis)
## Loading required package: viridisLite
# visualize each trade profit 
ret_data %>%
ggplot(aes(x = factor(Trade, labels = paste("Trade",Trade)), y = Returns, fill=Returns)) +
  geom_bar(stat = "identity",color='black',width=0.5) +
  labs(title = "Strategy Returns",
       x = "Trade",
       y = "Returns") +
  theme_minimal()+
  geom_text(aes(label = sprintf("%.2f%%", round(Returns * 100, 2)), vjust = -0.2), size = 4) +
  scale_fill_viridis()+
  scale_y_continuous(labels = scales::percent) 

# Create new dataframe that contains final return
final_ret_data <- data.frame(Trade = c("Buy & Hold","Strategy Return") , 
                             Returns = c(hold_ret,total_strategy_ret))

final_ret_data
##             Trade  Returns
## 1      Buy & Hold 1.053969
## 2 Strategy Return 1.300805
# Visualize the result
final_ret_data %>%
ggplot(aes(x = factor(Trade, labels = paste(Trade)), y = Returns, fill=Returns)) +
  geom_bar(stat = "identity",color='black',width=0.4) +
  labs(title = "Total Returns",
       x = "Trade",
       y = "Returns") +
  theme_minimal()+
  geom_text(aes(label = sprintf("%.2f%%", round(Returns * 100, 2)), vjust = -0.2), size = 4) +
  scale_fill_viridis()+
  scale_y_continuous(labels = scales::percent) 

From the result, we found out that Moving Average strategy was more effective than just buying and holding the stock. However, this does not mean that this strategy always work. We have to backtest for longer time horizon and it might not be effective for other stocks as well.