Creating Custom Models

Simon Mueller

2026-10-09

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:

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