In a previous post, we reviewed how to import the Fama French 3-Factor data, wrangle that data, and then regress our portfolio returns on the factors. Please have a look at that previous post, as the following work builds upon it. For more background on Fama French, see the original article published in The Journal of Financial Economics, Common risk factors in the returns on stocks and bonds.
Today, we will explore the rolling Fama French model and the explanatory power of the 3 factors in different time periods. In the financial world, we often look at rolling means, standard deviations and models to make sure we haven’t missed anything unusual, risky, or concerning during different market or economic regimes. Our portfolio returns history is for the years 2013 through 2017, which is rather a short history, but there still might a be a 24-month period where the Fama French factors were particularly strong, weak, or meaningless. We would like to unearth and hypothesize about what explains them or their future likelihood.
We will be working with our usual portfolio consisting of:
+ SPY (S&amp;P500 fund) weighted 25%
+ EFA (a non-US equities fund) weighted 25%
+ IJS (a small-cap value fund) weighted 20%
+ EEM (an emerging-mkts fund) weighted 20%
+ AGG (a bond fund) weighted 10%
Before we can run a Fama French model for that portfolio, we need to find portfolio monthly returns, which was covered in this post. I won’t go through the logic again but the code is here:
library(tidyquant)
library(tidyverse)
library(timetk)
symbols %
reduce(merge) %__% `colnames%
gather(asset, returns, -date) %__% group_by(asset) %__% mutate(returns = (log(returns) - log(lag(returns)))) %__% na.omit()
portfolio_returns_tq_rebalanced_monthly %
tq_portfolio(assets_col = asset, returns_col = returns,
weights = w,
col_rename = &quot;returns&quot;,
rebalance_on = &quot;months&quot;)
We also need to import the Fama French factors and combine them into one object with our portfolio returns. We painstakingly covered this in the previous post and the code for doing so is here:
temp % mutate(tidied = map(rolling_ff, tidy, conf.int = T)) %__% unnest(tidied) %__% slice(-1:-23) %__% select(date, term, estimate, conf.low, conf.high) %__% filter(term != &quot;(Intercept)&quot;) %__% rename(beta = estimate, factor = term) %__% group_by(factor)
head(rolling_ff_betas, 3)
# A tibble: 3 x 5
# Groups: factor [3]
date factor beta conf.low conf.high
1 2014-12-31 MKT_RF 0.931 0.784 1.08 2 2014-12-31 SMB -0.0130 -0.278 0.252
3 2014-12-31 HML -0.160 -0.459 0.139
We now have rolling betas and confidence intervals for each of our 3 factors. Let’s apply the same code logic and extract the rolling R-squared for our model. The only difference is we call glance() instead of tidy().
rolling_ff_rsquared % mutate(rolling_ff = rolling_lm(R_excess, MKT_RF, SMB, HML)) %__%
slice(-1:-23) %__%
mutate(glanced = map(rolling_ff, glance)) %__% unnest(glanced) %__% select(date, r.squared, adj.r.squared, p.value)
head(rolling_ff_rsquared, 3)
# A tibble: 3 x 4
date r.squared adj.r.squared p.value
1 2014-12-31 0.898 0.883 4.22e-10
2 2015-01-31 0.914 0.901 8.22e-11
3 2015-02-28 0.919 0.907 4.19e-11
We have extracted rolling factor betas and the rolling model R-squared, now let’s visualize.
Visualizing Rolling Fama French
We start by charting the rolling factor betas with ggplot(). This gives us an view into how the explanatory power of each factor has changed over time.
rolling_ff_betas %__% ggplot(aes(x = date, y = beta, color = factor)) + geom_line() +
labs(title= &quot;24-Month Rolling FF Factor Betas&quot;,
x = &quot;rolling betas&quot;) +
scale_x_date(breaks = scales::pretty_breaks(n = 10)) +
theme_minimal() +
theme(plot.title = element_text(hjust = 0.5),
axis.text.x = element_text(angle = 90)) The rolling factor beta chart reveals some interesting trends. Both SMB and HML have hovered around zero, while the MKT factor has hovered around 1. That’s consistent with our plot of betas with confidence intervals from last time.
Next, let’s visualize the rolling R-squared with highcharter.
We first convert rolling_ff_rsquared to xts, using the tk_xts() function.
rolling_ff_rsquared_xts %
tk_xts(date_var = date, silent = TRUE)
Then pass the xts object to a highchart(type = &quot;stock&quot;) code flow, adding the rolling R-squared time series with hc_add_series(rolling_ff_rsquared_xts$r.squared...).
highchart(type = &quot;stock&quot;) %__% hc_add_series(rolling_ff_rsquared_xts$r.squared,
color = &quot;cornflowerblue&quot;,
name = &quot;r-squared&quot;) %__% hc_title(text = &quot;Rolling FF 3-Factor R-Squared&quot;) %__%
hc_add_theme(hc_theme_flat()) %__%
hc_navigator(enabled = FALSE) %__% hc_scrollbar(enabled = FALSE) %__% hc_exporting(enabled = TRUE)
That chart looks choppy, but the R-squared never really left the range between .9 and .95. We can tweak the minimum and maximum y-axis values for some perspective.
highchart(type = &quot;stock&quot;) %__% hc_add_series(rolling_ff_rsquared_xts$r.squared,
color = &quot;cornflowerblue&quot;,
name = &quot;r-squared&quot;) %__% hc_title(text = &quot;Rolling FF 3-Factor R-Squared&quot;) %__%
hc_yAxis( max = 2, min = 0) %__% hc_add_theme(hc_theme_flat()) %__%
hc_navigator(enabled = FALSE) %__% hc_scrollbar(enabled = FALSE) %__% hc_exporting(enabled = TRUE)
Ah, when the y-axis is zoomed out a bit, our R-squared looks consistently near 1 for the life of the portfolio.
That’s all for today. Thanks and see you next time!