knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE )
The simple Market Model regresses firm returns on a single market index. While widely used, it may miss systematic risk factors that explain return variation. The EventStudy package provides several alternatives:
| Model | Class | Factors |
|-------|-------|---------|
| Fama-French 3-Factor | FamaFrench3FactorModel | Market, SMB, HML |
| Fama-French 5-Factor | FamaFrench5FactorModel | Market, SMB, HML, RMW, CMA |
| Carhart 4-Factor | Carhart4FactorModel | Market, SMB, HML, MOM |
| GARCH(1,1) | GARCHModel | Market (time-varying volatility) |
| BHAR | BHARModel | Market (compounded returns) |
Factor models reduce estimation-window residual variance, yielding more powerful tests. GARCH accounts for volatility clustering. BHAR avoids the bias from summing daily abnormal returns over long horizons.
library(EventStudy) library(dplyr) library(tibble) library(ggplot2)
factor_tblFactor models require a factor table passed to EventStudyTask$new(). This
table must contain a date column (same format as firm/index data) plus the
relevant factor columns.
| Model | Required columns in factor_tbl |
|-------|----------------------------------|
| Fama-French 3-Factor | smb, hml, risk_free_rate |
| Fama-French 5-Factor | smb, hml, rmw, cma, risk_free_rate |
| Carhart 4-Factor | smb, hml, mom, risk_free_rate |
The risk_free_rate column is special: when present, prepare_event_study()
automatically computes excess returns:
excess_return = firm_returns - risk_free_ratemarket_excess = index_returns - risk_free_rateThese are the dependent and independent variables used by the factor model regressions.
Fama-French factor data is publicly available from Kenneth French's website.
In practice you might use the frenchdata or tidyquant packages:
# Example: constructing a factor_tbl manually # In practice, download from Kenneth French's data library set.seed(42) n <- 300 dates <- format(seq(as.Date("2014-06-01"), by = "day", length.out = n), "%d.%m.%Y") factor_tbl <- tibble( date = dates, smb = rnorm(n, 0, 0.005), # Small Minus Big hml = rnorm(n, 0, 0.005), # High Minus Low rmw = rnorm(n, 0, 0.004), # Robust Minus Weak cma = rnorm(n, 0, 0.004), # Conservative Minus Aggressive mom = rnorm(n, 0, 0.006), # Momentum risk_free_rate = rep(0.0001, n) # Daily risk-free rate )
The three-factor model (Fama & French, 1993) explains stock returns using market risk, size (SMB), and value (HML) factors:
$$R_i - R_f = \alpha + \beta_m (R_m - R_f) + \beta_s \text{SMB} + \beta_h \text{HML} + \epsilon$$
set.seed(42) n <- 300 dates <- format(seq(as.Date("2014-06-01"), by = "day", length.out = n), "%d.%m.%Y") firm_tbl <- tibble( symbol = "FIRM_A", date = dates, adjusted = 100 * cumprod(1 + rnorm(n, 0.0003, 0.015)) ) index_tbl <- tibble( symbol = "INDEX_1", date = dates, adjusted = 1000 * cumprod(1 + rnorm(n, 0.0002, 0.012)) ) request_tbl <- tibble( event_id = 1L, firm_symbol = "FIRM_A", index_symbol = "INDEX_1", event_date = dates[200], group = "Test", event_window_start = -10L, event_window_end = 10L, shift_estimation_window = -11L, estimation_window_length = 150L ) # Pass factor_tbl to the task task <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl, factor_tbl = factor_tbl) params <- ParameterSet$new( return_calculation = LogReturn$new(), return_model = FamaFrench3FactorModel$new() ) task <- task |> prepare_event_study(params) |> fit_model(params) |> calculate_statistics(params)
After fitting, the model stores all factor coefficients:
# Model statistics for event 1 stats <- task$data_tbl$model[[1]]$statistics # Factor loadings stats$alpha # Intercept (Jensen's alpha) stats$beta # Market beta (same as `market_excess`) stats$smb # SMB loading stats$hml # HML loading stats$r2 # R-squared stats$sigma # Residual standard error # Full coefficient table task$get_model_stats(event_id = 1)
The five-factor model (Fama & French, 2015) adds profitability (RMW) and investment (CMA) factors:
$$R_i - R_f = \alpha + \beta_m (R_m - R_f) + \beta_s \text{SMB} + \beta_h \text{HML} + \beta_r \text{RMW} + \beta_c \text{CMA} + \epsilon$$
task_ff5 <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl, factor_tbl = factor_tbl) params_ff5 <- ParameterSet$new( return_calculation = LogReturn$new(), return_model = FamaFrench5FactorModel$new() ) task_ff5 <- task_ff5 |> prepare_event_study(params_ff5) |> fit_model(params_ff5) |> calculate_statistics(params_ff5)
The Carhart (1997) model adds a momentum factor (MOM) to the three-factor model:
$$R_i - R_f = \alpha + \beta_m (R_m - R_f) + \beta_s \text{SMB} + \beta_h \text{HML} + \beta_{mom} \text{MOM} + \epsilon$$
task_c4 <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl, factor_tbl = factor_tbl) params_c4 <- ParameterSet$new( return_calculation = LogReturn$new(), return_model = Carhart4FactorModel$new() ) task_c4 <- task_c4 |> prepare_event_study(params_c4) |> fit_model(params_c4) |> calculate_statistics(params_c4)
The GARCH model (Bollerslev, 1986) accounts for volatility clustering---the tendency of large returns to follow large returns. The mean equation includes the market return as a regressor (like the Market Model), but the variance is modeled as time-varying:
$$R_{i,t} = \mu + \beta R_{m,t} + \epsilon_t, \quad \epsilon_t \sim N(0, \sigma_t^2)$$ $$\sigma_t^2 = \omega + \alpha_1 \epsilon_{t-1}^2 + \beta_1 \sigma_{t-1}^2$$
The GARCH model requires the rugarch package:
install.packages("rugarch")
task_garch <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl) params_garch <- ParameterSet$new( return_model = GARCHModel$new() ) task_garch <- task_garch |> prepare_event_study(params_garch) |> fit_model(params_garch) |> calculate_statistics(params_garch)
The GARCH model stores conditional volatility from the estimation window:
stats <- task_garch$data_tbl$model[[1]]$statistics stats$alpha # Mean equation intercept stats$beta # Market beta stats$sigma # Average conditional sigma stats$garch_sigma # Time series of conditional sigmas
The time-varying conditional sigma can be particularly useful for standardized test statistics, as it accounts for periods of high and low volatility in the estimation window.
GARCH is most beneficial when:
For short estimation windows in stable markets, the simple Market Model is often sufficient.
Standard CARs sum daily abnormal returns:
$$CAR_i = \sum_{t=1}^{T} AR_{i,t}$$
For long horizons (months or years), this introduces a compounding bias. BHAR instead compounds returns, which better reflects the actual investor experience:
$$BHAR_i = \prod_{t=1}^{T} (1 + R_{i,t}) - \prod_{t=1}^{T} (1 + R_{m,t})$$
task_bhar <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl) params_bhar <- ParameterSet$new( return_model = BHARModel$new(), single_event_statistics = SingleEventStatisticsSet$new( tests = list(BHARTTest$new()) ) ) task_bhar <- task_bhar |> prepare_event_study(params_bhar) |> fit_model(params_bhar) |> calculate_statistics(params_bhar)
Unlike the Market Model where abnormal returns are computed per observation, BHAR computes running compounded differences. The abnormal return at each point represents the cumulative buy-and-hold difference up to that point:
# Tidy output tidy.EventStudyTask(task_bhar, type = "ar") # BHAR test statistic task_bhar$data_tbl$BHART[[1]]
The difference between BHAR and CAR grows with the horizon and the magnitude of returns:
# Short window: CAR and BHAR are nearly identical task_short <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl) params_short <- ParameterSet$new(return_model = MarketModel$new()) task_short <- run_event_study(task_short) car_short <- tidy.EventStudyTask(task_short, type = "car") bhar_short <- tidy.EventStudyTask(task_bhar, type = "ar")
For a 21-day event window (typical), the difference is usually negligible. For horizons of 6--12 months or longer, BHAR is strongly preferred.
A rigorous event study often compares results across multiple models to assess robustness:
models <- list( "Market Model" = MarketModel$new(), "FF3" = FamaFrench3FactorModel$new(), "FF5" = FamaFrench5FactorModel$new(), "Carhart" = Carhart4FactorModel$new() ) results <- purrr::imap_dfr(models, function(model, name) { t <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl, factor_tbl = factor_tbl) p <- ParameterSet$new( return_calculation = LogReturn$new(), return_model = model ) t <- t |> prepare_event_study(p) |> fit_model(p) |> calculate_statistics(p) tidy.EventStudyTask(t, type = "car") |> mutate(model = name) }) # Plot CARs across models ggplot(results, aes(x = relative_index, y = car, color = model)) + geom_line(linewidth = 0.8) + geom_hline(yintercept = 0, linetype = "dashed", color = "grey40") + geom_vline(xintercept = 0, linetype = "dotted", color = "red", alpha = 0.6) + labs( title = "CAR Comparison Across Models", x = "Relative Time to Event", y = "Cumulative Abnormal Return", color = "Model" ) + theme_minimal()
If the main result holds across all models, you have strong evidence that it is not an artifact of the return model specification.
| Model | Best for | Extra data needed |
|-------|----------|-------------------|
| Market Model | Short horizons, standard studies | None |
| FF3 | Controlling for size and value | factor_tbl with SMB, HML |
| FF5 | Adding profitability and investment | factor_tbl with SMB, HML, RMW, CMA |
| Carhart | Adding momentum exposure | factor_tbl with SMB, HML, MOM |
| GARCH | Volatile markets, heteroskedastic returns | None (requires rugarch) |
| BHAR | Long-horizon studies (6+ months) | None |
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.