Volume and Volatility Event Studies
Simon Mueller
2026-09-06
Source:vignettes/volume-volatility-event-study.Rmd
volume-volatility-event-study.RmdIntroduction
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.
Data Requirements
Volume Event Study
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.
Volatility Event Study
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.
Volume Event Study
Setup
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
)Running the Volume Study
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)Log Transform
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)Interpreting Results
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")Visualization
plot_event_study(task, type = "ar", event_id = 1) +
labs(title = "Abnormal Volume Around Earnings Announcement",
y = "Abnormal Log-Volume")Volatility Event Study
Setup
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
)Running the Volatility Study
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)Interpreting Results
Abnormal volatility is a variance ratio minus 1:
AV_{i,t} = \frac{R_{i,t}^2}{\hat{\sigma}_i^2} - 1
- AV = 0: volatility matches estimation-window expectations.
- AV > 0: volatility is higher than expected (e.g., information arrival).
- AV < 0: volatility is lower than expected (e.g., quiet trading).
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")Visualization
plot_event_study(task, type = "ar", event_id = 1) +
labs(title = "Abnormal Volatility Around Earnings Announcement",
y = "Abnormal Volatility (Variance Ratio - 1)")Combining Price, Volume, and Volatility
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.
Diagnostics
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.
Export
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"))References
- Campbell, J. Y. & Hentschel, L. (1992). No news is good news: An asymmetric model of changing volatility in stock returns. Journal of Financial Economics, 31(3), 281–318.
- Beaver, W. H. (1968). The information content of annual earnings announcements. Journal of Accounting Research, 6, 67–92.
- MacKinlay, A. C. (1997). Event Studies in Economics and Finance. Journal of Economic Literature, 35(1), 13–39.