knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE )
Traditional event studies focus on price-based abnormal returns. However, corporate events often affect trading volume and return volatility even when the net price effect is ambiguous. For example, an earnings announcement may trigger heavy trading and volatility spikes without moving the price much in either direction. Volume and volatility event studies capture these information-content effects that price-based methods can miss.
The EventStudy package provides two dedicated models:
| Model | Class | Abnormal measure |
|-------|-------|------------------|
| Volume | VolumeModel | Deviation of (log-)volume from estimation-window mean |
| Volatility | VolatilityModel | Ratio of squared returns to estimation-window variance, minus 1 |
Both models plug into the standard pipeline (prepare_event_study() →
fit_model() → calculate_statistics()) and are compatible with all existing
test statistics.
library(EventStudy) library(dplyr) library(ggplot2)
The VolumeModel requires a firm_volume column in the firm data, in
addition to the standard symbol, date, and adjusted columns. The volume
column should contain raw trading volume (number of shares traded):
firm_tbl <- tibble( symbol = rep("FIRM_A", 300), date = format(seq(as.Date("2014-01-01"), by = "day", length.out = 300), "%d.%m.%Y"), adjusted = 100 * cumprod(1 + rnorm(300, 0.0003, 0.015)), firm_volume = pmax(0, rnorm(300, mean = 1e6, sd = 2e5)) )
The firm_volume column is preserved through the pipeline automatically.
The standard EventStudyTask passes through any extra columns present in the
firm data.
The VolatilityModel does not require extra columns---it uses the firm
returns already computed by prepare_event_study(). Abnormal volatility is
defined as:
$$AV_{i,t} = \frac{R_{i,t}^2}{\hat{\sigma}_i^2} - 1$$
where $\hat{\sigma}_i^2$ is the variance of firm returns in the estimation window. Positive values indicate higher-than-expected volatility.
set.seed(42) # Firm data with volume n <- 300 firm_tbl <- tibble( symbol = "FIRM_A", date = format(seq(as.Date("2014-06-01"), by = "day", length.out = n), "%d.%m.%Y"), adjusted = 100 * cumprod(1 + rnorm(n, 0.0003, 0.015)), firm_volume = pmax(0, rnorm(n, mean = 1e6, sd = 2e5)) ) # Index data index_tbl <- tibble( symbol = "INDEX_1", date = firm_tbl$date, adjusted = 1000 * cumprod(1 + rnorm(n, 0.0002, 0.012)) ) # Request table request_tbl <- tibble( event_id = 1L, firm_symbol = "FIRM_A", index_symbol = "INDEX_1", event_date = firm_tbl$date[200], group = "Earnings", event_window_start = -10L, event_window_end = 10L, shift_estimation_window = -11L, estimation_window_length = 150L )
The key difference from a standard event study is passing VolumeModel$new()
as the return model. The study_type field on the ParameterSet can be set
to "volume" for appropriate axis labels in plots.
task <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl) params <- ParameterSet$new( return_calculation = SimpleReturn$new(), return_model = VolumeModel$new(log_transform = TRUE), single_event_statistics = SingleEventStatisticsSet$new(), multi_event_statistics = MultiEventStatisticsSet$new() ) params$study_type <- "volume" task <- task |> prepare_event_study(params) |> fit_model(params) |> calculate_statistics(params)
By default, VolumeModel log-transforms volume (log(volume + 1)) before
computing abnormal volume. This is standard practice because raw volume is
typically right-skewed. To disable the transform:
vm <- VolumeModel$new(log_transform = FALSE)
Abnormal volume is the difference between observed (log-)volume and the estimation-window mean:
$$AVolume_{i,t} = \log(V_{i,t} + 1) - \overline{\log(V_i + 1)}^{\text{est}}$$
Positive values indicate higher-than-normal trading activity. The test statistics (AR T-test, CAR T-test, etc.) are applied to these abnormal volume measures in the same way as for abnormal returns.
# Single-event results task$get_ar(event_id = 1) task$get_car(event_id = 1) # Tidy output tidy.EventStudyTask(task, type = "ar")
plot_event_study(task, type = "ar", event_id = 1) + labs(title = "Abnormal Volume Around Earnings Announcement", y = "Abnormal Log-Volume")
The volatility study uses the same data structure as a standard event study---no extra columns are needed.
set.seed(123) n <- 300 firm_tbl <- tibble( symbol = "FIRM_A", date = format(seq(as.Date("2014-06-01"), by = "day", length.out = n), "%d.%m.%Y"), adjusted = 100 * cumprod(1 + rnorm(n, 0.0003, 0.015)) ) index_tbl <- tibble( symbol = "INDEX_1", date = firm_tbl$date, 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 = firm_tbl$date[200], group = "Earnings", event_window_start = -10L, event_window_end = 10L, shift_estimation_window = -11L, estimation_window_length = 150L )
task <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl) params <- ParameterSet$new( return_calculation = SimpleReturn$new(), return_model = VolatilityModel$new(), single_event_statistics = SingleEventStatisticsSet$new(), multi_event_statistics = MultiEventStatisticsSet$new() ) params$study_type <- "volatility" task <- task |> prepare_event_study(params) |> fit_model(params) |> calculate_statistics(params)
Abnormal volatility is a variance ratio minus 1:
$$AV_{i,t} = \frac{R_{i,t}^2}{\hat{\sigma}_i^2} - 1$$
Values around event dates are typically large and positive when events carry new information, regardless of the direction of the price move.
task$get_ar(event_id = 1) task$get_car(event_id = 1) tidy.EventStudyTask(task, type = "ar")
plot_event_study(task, type = "ar", event_id = 1) + labs(title = "Abnormal Volatility Around Earnings Announcement", y = "Abnormal Volatility (Variance Ratio - 1)")
A comprehensive event study often examines all three dimensions. You can run separate studies on the same data and compare results:
set.seed(42) n <- 300 firm_tbl <- tibble( symbol = "FIRM_A", date = format(seq(as.Date("2014-06-01"), by = "day", length.out = n), "%d.%m.%Y"), adjusted = 100 * cumprod(1 + rnorm(n, 0.0003, 0.015)), firm_volume = pmax(0, rnorm(n, mean = 1e6, sd = 2e5)) ) index_tbl <- tibble( symbol = "INDEX_1", date = firm_tbl$date, 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 = firm_tbl$date[200], group = "Earnings", event_window_start = -10L, event_window_end = 10L, shift_estimation_window = -11L, estimation_window_length = 150L ) # --- Price study --- task_price <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl) params_price <- ParameterSet$new(return_model = MarketModel$new()) task_price <- run_event_study(task_price, params_price) # --- Volume study --- task_vol <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl) params_vol <- ParameterSet$new(return_model = VolumeModel$new()) params_vol$study_type <- "volume" task_vol <- task_vol |> prepare_event_study(params_vol) |> fit_model(params_vol) |> calculate_statistics(params_vol) # --- Volatility study --- task_volat <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl) params_volat <- ParameterSet$new(return_model = VolatilityModel$new()) params_volat$study_type <- "volatility" task_volat <- task_volat |> prepare_event_study(params_volat) |> fit_model(params_volat) |> calculate_statistics(params_volat)
# Extract tidy ARs from each study ar_price <- tidy.EventStudyTask(task_price, type = "ar") %>% mutate(study = "Price (AR)") ar_vol <- tidy.EventStudyTask(task_vol, type = "ar") %>% mutate(study = "Volume (Abnormal Log-Volume)") ar_volat <- tidy.EventStudyTask(task_volat, type = "ar") %>% mutate(study = "Volatility (Variance Ratio - 1)") combined <- bind_rows(ar_price, ar_vol, ar_volat) ggplot(combined, aes(x = relative_index, y = abnormal_returns)) + geom_hline(yintercept = 0, linetype = "dashed", colour = "grey40") + geom_vline(xintercept = 0, linetype = "dotted", colour = "red", alpha = 0.6) + geom_col(fill = "steelblue", alpha = 0.7) + facet_wrap(~ study, ncol = 1, scales = "free_y") + labs( title = "Price, Volume, and Volatility Event Study", x = "Relative Time to Event", y = "Abnormal Measure" ) + theme_minimal()
This three-panel view reveals whether the event primarily affects prices, trading activity, volatility, or some combination.
Model diagnostics work the same way as for price-based studies:
# Volume model diagnostics model_diagnostics(task_vol) plot_diagnostics(task_vol, event_id = 1) # Volatility model diagnostics model_diagnostics(task_volat) plot_diagnostics(task_volat, event_id = 1)
The diagnostics include estimation-window residual normality (Shapiro-Wilk), autocorrelation (Durbin-Watson, Ljung-Box), and model fit statistics.
Results can be exported in the same formats as any other study:
export_results(task_vol, "volume_results.csv") export_results(task_vol, "volume_results.xlsx") export_results(task_volat, "volatility_results.tex", which = c("model", "ar"))
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.