Skip to contents

Introduction

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.

The ModelBase Interface

All models must implement two methods:

  1. fit(data_tbl) – Estimate model parameters from the estimation window.
  2. 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.

Example: Industry-Adjusted Model

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
      )
    }
  )
)

Using the Custom Model

Once defined, use your model exactly like any built-in model:

# Create parameter set with custom model
params <- ParameterSet$new(
  return_model = IndustryAdjustedModel$new()
)

# Run the event study
task <- EventStudyTask$new(firm_data, index_data, request_data)
task <- run_event_study(task, params)

Example: Excess Return 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)
    }
  )
)

Using Factor Models

The package includes several built-in factor models that inherit from LinearFactorModel:

  • FamaFrench3FactorModel – SMB, HML, market excess
  • FamaFrench5FactorModel – adds RMW, CMA
  • Carhart4FactorModel – 3-factor + momentum

These 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)

Key Points

  • Always inherit from ModelBase
  • Implement fit() and abnormal_returns()
  • Populate private$.statistics$sigma and private$.statistics$degree_of_freedom
  • Use private$add_residuals() and private$first_order_autocorrelation() for diagnostics compatibility
  • Set private$.is_fitted <- TRUE after successful fitting
  • The model is deep-cloned for each event, so you can store event-specific state