[This article was first published on Return and Risk, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
Another hotly anticipated FOMC meeting kicks off next week, so I thought it would be timely to highlight a less well-known working paper, “Stock Returns over the FOMC Cycle”, by Cieslak, Morse and Vissing-Jorgensen (current draft June 2014). Its main result is:
Over the last 20 years, the average excess return on stocks over Treasury bills follows a bi-weekly pattern over the Federal Open Market Committee meeting cycle. The equity premium over this 20-year period was earned entirely in weeks 0, 2, 4 and 6 in FOMC cycle time, with week 0 starting the day before a scheduled FOMC announcement day.
In this post, we’ll look to recreate their cycle pattern and then backtest a trading strategy to test the claim of economic significance. Another objective is to evaluate the R package Quantstrat “for constructing trading systems and simulation.”
As there is not a lot of out-of-sample data since the release of the paper in 2014, we’ll use all the data to detect the pattern, and then proceed to check the impact of transaction costs on the economic significance of one possible FOMC cycle trading strategy.
################################################################################
# install packages and load them #
################################################################################
install.packages("RCurl", repos = "http://cran.us.r-project.org")
install.packages("quantstrat", repos="http://R-Forge.R-project.org")
library(RCurl)
library(quantstrat)
################################################################################
# get data - Jan 1994 to Mar 2015 #
################################################################################
# download csv file data of FOMC announcement dates from previous post
csvfile = getURLContent(
"https://docs.google.com/uc?export=download&id=0B4oNodML7SgSckhUUWxTN1p5VlE",
ssl.verifypeer = FALSE, followlocation = TRUE, binary = FALSE)
fomcdatesall <- read.csv(textConnection(csvfile), colClasses = c(rep("Date", 2),
rep("numeric", 2), rep("character", 2)), stringsAsFactors = FALSE)
# set begin and end dates
beg.date <- "1994-01-01"
end.date <- "2015-03-09"
last.fomc.date <- "2015-03-17"
# get S&P500 ETF prices
getSymbols(c("SPY"), from = beg.date, to = end.date)
# subset fomc dates
fomc.dates <- subset(fomcdatesall, begdate > as.Date(beg.date) &
begdate <= as.Date(last.fomc.date) &
scheduled == 1, select = c(begdate, enddate))
FOMC Cycle Pattern
The chart and table below clearly show the bi-weekly pattern over the FOMC Cycle of Cieslak et al in SPY 5-day returns. This is based on calendar weekdays (i.e. day count includes holidays), with week 0 starting one day before a scheduled FOMC announcement day (i.e. on day -1). Returns in even weeks (weeks 0, 2, 4, 6) are positive, while those in odd weeks (weeks -1, 1, 3, 5) are lower and mostly slightly negative.
################################################################################
# custom indicator function for fomc cycle #
# calculates cycle day, week and phase #
################################################################################
get.fomc.cycle <- function(mktdata, fomcdates, begdate, enddate) {
# create time series with all weekdays incl. holidays
indicator <- xts(order.by = seq(as.Date(begdate),
as.Date(as.numeric(last(fomc.dates)[2])), by = 1))
indicator <- merge(indicator, mktdata)
indicator <- indicator[which(weekdays(index(indicator)) %in%
c("Monday", "Tuesday", "Wednesday", "Thursday", "Friday")), ]
indicator <- na.locf(indicator)
names(indicator) <- "close"
indicator$week <- indicator$day <- NA
indicator$phase <- NA
# get fomc cycle data
numdates <- nrow(fomcdates)
for (i in 1:numdates) {
cycle.beg <- which(index(indicator) == fomcdates[i, "enddate"]) - 6
if (i < numdates) {
cycle.end <- which(index(indicator) == fomcdates[i + 1, "enddate"]) - 6
} else {
cycle.end <- nrow(indicator)
}
# calculate cycle window, day and week counts
win <- window(index(indicator), cycle.beg, cycle.end)
win.len <- length(win)
day <- seq(-6, win.len - 7)
week <- rep(-1:7, each = 5, length.out = win.len)
# identify up and down phases
phase <- rep(c(-1, 1), each = 5, length.out = win.len)
# combine data
indicator[cycle.beg:cycle.end, c("day", "week", "phase")] <- c(day, week, phase)
}
# fix for day number > 33 ie keep as week 6 up-phase
# (only 3 instances 1994-2014, so not material)
indicator$phase[which(indicator$day > 33)] <- 0 # 1
# shift phase forward 2 days to force quantstrat trades to be executed on
# close of correct day ie this is a hack
indicator$phase.shift <- lag(indicator$phase, -2)
return(indicator[paste0(begdate, "::", enddate), ])
}
# get fomc cycle indicator data
fomc.cycle <- get.fomc.cycle(Ad(SPY), fomc.dates, beg.date, end.date)
# calculate 1-day and 5 day returns
fomc.cycle$ret1day <- ROC(fomc.cycle$close, n = 1, type = "discrete")
fomc.cycle$ret5day <- lag(ROC(fomc.cycle$close, n = 5, type = "discrete"), -4)
# calculate average 5-day return based on day in fomc cycle
rets <- tapply(fomc.cycle$ret5day, fomc.cycle$day, mean, na.rm = TRUE)[1:40] * 100
# plot cycle graph
plot(-6:33, rets, type = "l",
xlab = "Days since FOMC meeting (weekends excluded)",
ylab = "Avg 5-day return, t0 to t4 (%)",
main = "SPY Average 5-day Return over FOMC CyclernJan 1994 - Mar 2015",
xaxt = "n")
axis(1, at = seq(-6, 33, by = 1))
points(-6:33, rets)
abline(h = seq(-0.2, 0.6, 0.2), col = "gray")
points(seq(-6, 33, 10), rets[seq(1, 40, 10)], col = "red", bg = "red", pch = 25)
points(seq(-1, 33, 10), rets[seq(6, 40, 10)], col = "blue", bg = "blue", pch = 24)
text(-6:33, rets, -6:33, adj = c(-0.25, 1.25), cex = 0.7)
# get spy close mktdata for quantstrat
spy <- fomc.cycle$close
Economic Significance: FOMC Cycle Trading Strategy Using Quantstrat
In this section, we’ll create a trading strategy using the R Quantstrat package to test the claim of economic significance of the pattern. Note, Quantstrat is “still in heavy development” and as such is not available on CRAN but needs to be downloaded from the development web site. Nonetheless, it's been around for some time and it should be up to the backtesting task…
Based on the paper’s main result and our table above confirming the up-phase is more profitable, we’ll backtest a long only strategy that buys the SPY on even weeks (weeks 0, 2, 4, 6) and holds for 5 calendar days only, and compare it to a buy and hold strategy. In addition, we’ll look at the effect of transaction costs on overall returns.
A few things to note:
We’ll use a bet size of 100% of equity for all trades. This may not be optimal in developing trading systems but will allow for easy comparison with the buy and hold passive strategy, which is 100% allocated
Assume 5 basis points (0.05%) in execution costs (including commission and slippage), and initial equity of $100,000
Execution occurs on the close of the same day that the buy/sell signal happens. Unfortunately, Quantstrat does not allow this out-of-the-box, so we need to do a hack - a custom indicator function that shifts the signals forward in time (see “get.fomc.cycle” function above)
The following are the resulting performance metrics for the trading strategy, using 5 basis points for transaction costs, and comparisons with the passive buy and hold strategy (before and after transaction costs).
Before transaction costs, we were able to reproduce similar results to the paper, with the long only strategy of buying the SPY in even weeks and holding for 5 days. In our case, this strategy added about 2% p.a. to buy and hold returns, reduced volatility by 30% and increased the Sharpe ratio by 70% to 0.82 (from 0.47).
However, after allowing for a reasonable 5 basis points (0.05%) in execution costs, annualized returns fall below that of the buy and hold strategy (9.15%) to 8.55%. As volatility remains lower, this means the risk-adjusted performance is better by only 30% now (Sharpe ratio of 0.62). See table below for details.
Buy and Hold
Long Only before Transaction Costs
Long Only with 5bp Transaction Costs
Annualized Return
0.0915
0.1129
0.0855
Annualized Std Dev
0.1935
0.1382
0.1382
Annualized Sharpe (Rf=0%)
0.4727
0.8169
0.6183
Execution costs (brokerage and slippage) can have a material impact on trading system performance. So the key takeaway is to be explicit in accounting for them when claiming economic significance. There are a lot of backtests out there that don’t…
Quantstrat
There is a bit of a learning curve with the Quantstrat package but once you get used to it, it’s a solid backtesting platform. In addition, it has other capabilities like optimization and walk-forward testing.
The main issue I have is that it doesn’t natively allow you to execute on the daily close when you get a signal on that day’s close - you need to do a hack. This puts it at a bit of a disadvantage to other software like TradeStation, MultiCharts, NinjaTrader and Amibroker (presumably MatLab too). Hopefully the developers will reconsider this, to help drive higher adoption of their gReat package…