Strategy – S&P500 Seasonal

During a previous post, S&P500 Seasonal Study + Other Commodities: https://flare9xblog.wordpress.com/2017/10/12/seasonal-study/

It was determined that the best months to be long the S&P500 was from months October through to the end of May.

As a refresher:

Rplot126
S&P500 Mean Daily Spread Per Month

We back test this theme to see how it performs over monthly bars from 1960 to present day (10.12.2017):

Rplot136

SP500seasoal.results

What we see is that we are invested 67% of the time being long from the Open bar of October and selling at the Close of May. We do beat the S&P500 and I believe this is partly due to drawing down less post 1999 bear market. Our maximum draw down is slightly less than than buy and hold at 43%.

What stands out is the average gain during this October to May holding period. We see that the average trade is 14.3%.

To see some relative benchmark, lets see what it looks like only buying May open and selling at the Close of September:

Rplot137

may to sep

> Return.cumulative(xts1, geometric = TRUE)
                        [,1]
Cumulative Return -0.1973985

We see cumulative returns for Buying May open and selling Close of September are -0.1973985. That is the growth of $1. Essentially May to September is negative to flat. The cumulative returns do not even warrant shorting May to September.

In closing we see that the majority of the S&P500 gains seem to be attributed to seasonal trends specifically buying in October and Selling in May.

Full back test R Code for the above


# S&P500 Seasonal Back Test 
# Month Bars
# Buy Open of October, Sell End of April (Based on seasonal study)
# Andrew Bannerman 10.12.2017

require(lubridate)
require(dplyr)
require(magrittr)
require(TTR)
require(zoo)
require(data.table)
require(xts)
require(PerformanceAnalytics)

# Data path
data.dir <- "C:/R Projects/Final Scripts/Seasonal Strategies/Data"
data.read.spx <- paste(data.dir,"$SPX.Monthly.csv",sep="/")

# Read data
read.spx <- read.csv(data.read.spx,header=TRUE, sep=",",skip=0,stringsAsFactors=FALSE)

# Make dataframe 
new.df <- data.frame(read.spx)

# Convert Values To Numeric 
cols <-c(3:8)
new.df[,cols] %<>% lapply(function(x) as.numeric(as.character(x)))

# Convert Date Column [1]
new.df$Date <- ymd(new.df$Date)

# Add day of month column 
# This is a helper column for creating buy/sell rules
group <- new.df %>% dplyr::mutate(month = lubridate::month(Date)) %>% group_by(month) 
new.df <- data.frame(new.df,group$month)

# Calculate Returns from open to close 
new.df$ocret <- apply(new.df[,c('Open', 'Close')], 1, function(x) { (x[2]-x[1])/x[1]} )

# Calculate Close-to-Close returns
new.df$clret <- ROC(new.df$Close, type = c("discrete"))
new.df$clret[1] <- 0

# Subset by Date
new.df <- subset(new.df, Date >= as.POSIXct("1960-01-01") )  

# Enter Long Rules 
# Buy October To End of April
x = new.df$group.month
# October to May Hold Period Rules
#new.df$enter <- ifelse(x == 1,1,0 | ifelse(x == 2,1,0 | ifelse(x == 3,1,0 | ifelse(x == 4,1,0 | ifelse(x==5,1,0 | ifelse(x == 10,1,0 | ifelse(x == 11,1,0 | ifelse(x == 12,1,0))))))))
#May to September Hold Period Rules
new.df$enter <- ifelse(x == 5,1,0 | ifelse(x == 6,1,0 | ifelse(x == 7,1,0 | ifelse(x == 8,1,0 | ifelse(x==9,1,0)))))

# Calculate equity curves
# Long
new.df <- new.df %>%
  dplyr::mutate(RunID = rleid(enter)) %>%
  group_by(RunID) %>%
  dplyr::mutate(seasonal.equity = ifelse(enter == 0, 0,
                                         ifelse(row_number() == 1, ocret, clret))) %>%
  ungroup() %>%
  select(-RunID)

# Pull select columns from data frame to make XTS whilst retaining formats 
xts1 = xts(new.df$seasonal.equity, order.by=as.POSIXct(new.df$Date, format="%Y-%m-%d")) 
xts2 = xts(new.df$clret, order.by=as.POSIXct(new.df$Date, format="%Y-%m-%d")) 
str(new.df)

# Join XTS together 
compare <- cbind(xts1,xts2)

# Use the PerformanceAnalytics package for trade statistics
colnames(compare) <- c("Seasonal","Buy And Hold")
charts.PerformanceSummary(compare,main="Long May Open To Close of September", wealth.index=TRUE, colorset=rainbow12equal)
performance.table <- rbind(table.AnnualizedReturns(compare),maxDrawdown(compare), CalmarRatio(compare),table.DownsideRisk(compare))
drawdown.table <- rbind(table.Drawdowns(compare))
#dev.off()
#logRets <- log(cumprod(1+compare))
#chart.TimeSeries(logRets, legend.loc='topleft', colorset=rainbow12equal)

print(performance.table)
print(drawdown.table)

# Find net trade result of multiple 'n' day trades
# Find start day of trade, find end day, perform (last(Close) - first(Open))/first(Open) % calculation
new.df <- new.df %>%
  dplyr::mutate(RunID = data.table::rleid(enter)) %>%
  group_by(RunID) %>%
  dplyr::mutate(perc.output = ifelse(enter == 0, 0,
                                     ifelse(row_number() == n(),
                                            (last(Close) - first(Open))/first(Open), 0))) %>%
  ungroup() %>%
  select(-RunID)

# Win / Loss % 
# All Holding Months
winning.trades <- sum(new.df$seasonal.equity > '0', na.rm=TRUE)
losing.trades <- sum(new.df$seasonal.equity < '0', na.rm=TRUE)
even.trades <- sum(new.df$seasonal.equity == '0', na.rm=TRUE)
total.days <- NROW(new.df$seasonal.equity)

# Multi Month Trades
multi.winning.trades <- sum(new.df$perc.output > '0', na.rm=TRUE)
multi.losing.trades <- sum(new.df$perc.output < '0', na.rm=TRUE)
multi.total.days <- multi.winning.trades+multi.losing.trades

# % Time Invested
time.invested <- (winning.trades + losing.trades) / total.days
winning.trades + losing.trades

# Calcualte win loss %
# All months
total <- winning.trades + losing.trades
win.percent <- winning.trades / total
loss.percent <- losing.trades / total
# Multi Month Trades
multi.total <- multi.winning.trades + multi.losing.trades
multi.win.percent <- multi.winning.trades / multi.total
multi.loss.percent <- multi.losing.trades / multi.total
# Calculate Consecutive Wins Loss 
# All Months
remove.zero <- new.df[-which(new.df$seasonal.equity == 0 ), ] # removing rows 0 values 
consec.wins <- max(rle(sign(remove.zero$seasonal.equity))[[1]][rle(sign(remove.zero$seasonal.equity))[[2]] == 1])
consec.loss <- max(rle(sign(remove.zero$seasonal.equity))[[1]][rle(sign(remove.zero$seasonal.equity))[[2]] == -1])
consec.wins

# Multi Month Trades
multi.remove.zero <- new.df[-which(new.df$perc.output == 0 ), ] # removing rows 0 values 
multi.consec.wins <- max(rle(sign(multi.remove.zero$perc.output))[[1]][rle(sign(multi.remove.zero$perc.output))[[2]] == 1])
multi.consec.loss <-max(rle(sign(multi.remove.zero$perc.output))[[1]][rle(sign(multi.remove.zero$perc.output))[[2]] == -1])

# Calculate Summary Statistics
# All Months
average.trade <- mean(new.df$seasonal.equity)
average.win <- mean(new.df$seasonal.equity[new.df$seasonal.equity >0])
average.loss <- mean(new.df$seasonal.equity[new.df$seasonal.equity <0])
median.win <- median(new.df$seasonal.equity[new.df$seasonal.equity >0])
median.loss <- median(new.df$seasonal.equity[new.df$seasonal.equity <0])
max.gain <- max(new.df$seasonal.equity)
max.loss <- min(new.df$seasonal.equity)
win.loss.ratio <- winning.trades / abs(losing.trades)
summary <- cbind(winning.trades,losing.trades,even.trades,total.days,win.percent,loss.percent,win.loss.ratio,time.invested,average.trade,average.win,average.loss,median.win,median.loss,consec.wins,consec.loss,max.gain,max.loss)
summary <- as.data.frame(summary)
colnames(summary) <- c("Winning Trades","Losing Trades","Even Trades","Total Trades","Win %","Loss %","Win Loss Ratio","Time Invested","Average Trade","Average Win","Average Loss","Median Gain","Median Loss","Consec Wins","Consec Loss","Maximum Win","Maximum Loss")
print(summary)

# Multi Month Trades
multi.average.trade <- mean(new.df$perc.output)
multi.average.win <- mean(new.df$perc.output[new.df$perc.output >0])
multi.average.loss <- mean(new.df$perc.output[new.df$perc.output <0])
multi.median.win <- median(new.df$perc.output[new.df$perc.output >0])
multi.median.loss <- median(new.df$perc.output[new.df$perc.output <0])
multi.win.loss.ratio <- multi.average.win / abs(multi.average.loss)
multi.max.gain <- max(new.df$perc.output)
multi.max.loss <- min(new.df$perc.output)
multi.summary <- cbind(multi.winning.trades,multi.losing.trades,multi.total.days,multi.win.percent,multi.loss.percent,multi.win.loss.ratio,time.invested,multi.average.trade,multi.average.win,multi.average.loss,multi.median.win,multi.median.loss,multi.consec.wins,multi.consec.loss,multi.max.gain,multi.max.loss)
multi.summary <- as.data.frame(multi.summary)
colnames(multi.summary) <- c("Winning Trades","Losing Trades","Total Trades","Win %","Loss %","Win Loss Ratio","Time Invested","Average Trade","Average Win","Average Loss","Median Gain","Median Loss","Consec Wins","Consec Loss","Maximum Win","Maximum Loss")
print(multi.summary)
print(performance.table)
print(drawdown.table)
table.Drawdowns(xts1, top=10)
Return.cumulative(xts1, geometric = TRUE)

# Write output to file
write.csv(new.df,file="C:/R Projects/SP500.Seasonal.csv")

Author: Andrew Bannerman

Integrity Inspector. Quantitative Analysis is a favorite past time.

2 thoughts on “Strategy – S&P500 Seasonal”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s