The EventStudy package uses an object-oriented model system based on
R6 classes. Every model inherits from ModelBase, which
defines the interface that the pipeline expects. This vignette shows how
to create your own custom model and plug it into the event study
workflow.
All models must implement two methods:
fit(data_tbl) – Estimate model
parameters from the estimation window.abnormal_returns(data_tbl) – Compute
abnormal returns for all observations (estimation + event windows).The fit() method should also populate
private$.statistics with at least sigma and
degree_of_freedom, which are used by the test
statistics.
Suppose you want an industry-adjusted model where the expected return equals the industry index return rather than the broad market index.
library(EventStudy)
IndustryAdjustedModel <- R6::R6Class("IndustryAdjustedModel",
inherit = ModelBase,
public = list(
model_name = "IndustryAdjustedModel",
fit = function(data_tbl) {
# No estimation needed -- just compute statistics
private$.is_fitted <- TRUE
private$calculate_statistics(data_tbl)
},
abnormal_returns = function(data_tbl) {
# AR = firm return - industry index return
# Assumes data_tbl has an 'industry_return' column
data_tbl %>%
dplyr::mutate(abnormal_returns = firm_returns - industry_return)
}
),
private = list(
calculate_statistics = function(data_tbl) {
estimation_tbl <- data_tbl %>%
dplyr::filter(estimation_window == 1)
residuals <- estimation_tbl$firm_returns - estimation_tbl$industry_return
private$add_residuals(residuals)
private$first_order_autocorrelation(residuals)
sigma <- sd(residuals, na.rm = TRUE)
private$.statistics$sigma <- sigma
private$.statistics$degree_of_freedom <- length(residuals) - 1
# Forecast error correction
event_window_tbl <- data_tbl %>% dplyr::filter(event_window == 1)
private$calculate_forecast_error_correction(
sigma, nrow(estimation_tbl),
estimation_tbl$index_returns,
event_window_tbl$index_returns
)
}
)
)Once defined, use your model exactly like any built-in model:
A model that simply computes excess returns over the risk-free rate:
ExcessReturnModel <- R6::R6Class("ExcessReturnModel",
inherit = ModelBase,
public = list(
model_name = "ExcessReturnModel",
risk_free_rate = 0.0001, # daily risk-free rate
initialize = function(risk_free_rate = 0.0001) {
self$risk_free_rate <- risk_free_rate
},
fit = function(data_tbl) {
private$.is_fitted <- TRUE
estimation_tbl <- data_tbl %>%
dplyr::filter(estimation_window == 1)
residuals <- estimation_tbl$firm_returns - self$risk_free_rate
private$.statistics$sigma <- sd(residuals, na.rm = TRUE)
private$.statistics$degree_of_freedom <- length(residuals) - 1
private$add_residuals(residuals)
private$first_order_autocorrelation(residuals)
},
abnormal_returns = function(data_tbl) {
data_tbl %>%
dplyr::mutate(abnormal_returns = firm_returns - self$risk_free_rate)
}
)
)The package includes several built-in factor models that inherit from
LinearFactorModel:
FamaFrench3FactorModel – SMB, HML, market excessFamaFrench5FactorModel – adds RMW, CMACarhart4FactorModel – 3-factor + momentumThese require factor data to be provided via the
factor_tbl argument in EventStudyTask:
# Factor data with date column matching firm/index data
factor_data <- tibble::tibble(
date = dates,
smb = smb_returns,
hml = hml_returns,
risk_free_rate = rf_rate
)
task <- EventStudyTask$new(firm_data, index_data, request_data,
factor_tbl = factor_data)
params <- ParameterSet$new(
return_model = FamaFrench3FactorModel$new()
)
task <- run_event_study(task, params)ModelBasefit() and
abnormal_returns()private$.statistics$sigma and
private$.statistics$degree_of_freedomprivate$add_residuals() and
private$first_order_autocorrelation() for diagnostics
compatibilityprivate$.is_fitted <- TRUE after successful
fitting