A comprehensive, modular R package for financial event study analysis. Implements the classical methodology (MacKinlay 1997) and extends it with modern multi-factor models, long-horizon methods, panel (DiD) event studies, and more.
EventStudy is part of the eventstudy.de toolkit -- "Event Study Analysis Made Simple":
install.packages("devtools")
# devtools::install_github("sipemu/eventstudy")
install.packages("EventStudy", repos = c('https://sipemu.r-universe.dev', 'https://cloud.r-project.org'))
Two Claude Code Agent Skills ship with this repo as an installable plugin, giving your AI assistant grounded knowledge of the package API:
es-capabilities -- discover the full API surface: return models, test
statistics, task types, function signatures, and method-for-task routing.es-advisor -- run the grounded load -> run -> diagnose -> advise -> re-run ->
compare loop (offline degrade path, never fabricates a number).Install via the built-in plugin marketplace:
/plugin marketplace add sipemu/eventstudy
/plugin install eventstudy@eventstudy
The skills are then auto-invoked from their descriptions, or callable as
/eventstudy:es-capabilities and /eventstudy:es-advisor.
AI Advisor: Grounded, LLM-agnostic advice via es_advise() plus offline es_diagnostics(). Interprets only package-computed numbers and never fabricates results. Works fully offline with no API key required; optional LLM providers (Anthropic, OpenAI-compatible, custom) can be supplied via provider().
One-Call Reporting: Generate a complete, multi-format event study report in a single call: es_report(task) -- deep-clones the task, harvests diagnostics, and renders HTML/PDF/Word/Markdown with a grounded narrative (offline by default). Also available as run_event_study(..., report = TRUE).
15+ Return Models: Market Model, Market Adjusted, Mean Adjusted, Fama-French 3- and 5-factor, Carhart 4-factor, GARCH(1,1), DCC-GARCH, Rolling-Window, Buy-and-Hold Abnormal Returns (BHAR), Volume, and Volatility models.
12 Test Statistics: Parametric (AR T, CAR T, BHAR T, Cross-Sectional T, Patell Z, BMP, Kolari-Pynnonen) and non-parametric (Sign, Generalized Sign, Rank, Calendar-Time Portfolio).
Diagnostics: Shapiro-Wilk normality, Durbin-Watson, Ljung-Box autocorrelation, pre-trend testing.
Visualization: Event study plots (AR, CAR, AAR, CAAR) with confidence bands, diagnostic plots, CAR distribution histograms, panel event study plots.
Result Export: CSV, Excel (multi-sheet), and LaTeX (publication-ready tables). Broom-compatible tidy() method for tidyverse workflows.
Cross-Sectional Analysis: Regress CARs on firm characteristics with HC-robust standard errors. Group comparisons, quantiles, and distribution plots.
Intraday Event Studies: POSIXct timestamp support with minute/second-level windows.
Panel Event Studies (DiD): Two-way fixed effects (static and dynamic), Sun & Abraham (2021) interaction-weighted estimator for staggered treatment.
Extensible Design: Add custom models (inherit ModelBase) and test statistics (inherit TestStatisticBase).
library(EventStudy)
# Create task from your data
task <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl)
# Run with default settings (Market Model, simple returns, all test statistics)
task <- run_event_study(task)
# Inspect results
print(task)
summary(task)
New in 0.60.0. Get grounded advice about your fitted study — fully offline, no API key, and it never invents numbers (it interprets only what the package computed):
diag <- es_diagnostics(task)
advice <- es_advise(diag, task_type = "recommend_stat")
print(advice)
Runs offline out of the box. To use an LLM for prose interpretation, pass an optional provider() (Anthropic, OpenAI-compatible, or custom).
The best entry point for understanding the advisor is the "The Grounded AI Advisor" vignette (vignette("ai-advisor")), which walks through the bundled dieselgate dataset (data(dieselgate) — Volkswagen vs. the DAX around the 2015 EPA disclosure), runs the deterministic advisor layer live, and shows the LLM layer as a captured static example.
Analyzing the 2015 Volkswagen emissions scandal and its impact on automotive stock prices.
library(tidyquant)
library(dplyr)
library(EventStudy)
index_symbol <- "^GDAXI"
firm_symbols <- c("VOW.DE", "PAH3.DE", "BMW.DE", "MBG.DE")
request_tbl <- tibble(
event_id = 1:4,
firm_symbol = firm_symbols,
index_symbol = rep(index_symbol, 4),
event_date = rep("18.09.2015", 4),
group = c(rep("VW Group", 2), rep("Other", 2)),
event_window_start = rep(-10, 4),
event_window_end = rep(10, 4),
shift_estimation_window = rep(-11, 4),
estimation_window_length = rep(250, 4)
)
firm_tbl <- firm_symbols %>%
tq_get(from = "2014-06-01", to = "2015-11-01") %>%
mutate(date = format(date, "%d.%m.%Y")) %>%
select(symbol, date, adjusted)
index_tbl <- index_symbol %>%
tq_get(from = "2014-06-01", to = "2015-11-01") %>%
mutate(date = format(date, "%d.%m.%Y")) %>%
select(symbol, date, adjusted)
One-liner with defaults:
task <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl)
task <- run_event_study(task)
Step-by-step with custom parameters:
task <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl)
est_params <- ParameterSet$new(
return_calculation = LogReturn$new(),
return_model = MarketModel$new(),
single_event_statistics = SingleEventStatisticsSet$new(),
multi_event_statistics = MultiEventStatisticsSet$new()
)
task <- prepare_event_study(task, est_params)
task <- fit_model(task, est_params)
task <- calculate_statistics(task, est_params)
Pipe-friendly:
task <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl)
est_params <- ParameterSet$new(return_calculation = LogReturn$new())
task |>
prepare_event_study(est_params) |>
fit_model(est_params) |>
calculate_statistics(est_params)
# Overview
print(task)
summary(task)
# Single-event results
task$get_ar(event_id = 1) # Abnormal returns
task$get_car(event_id = 1) # Cumulative abnormal returns
task$get_model_stats(event_id = 1) # Model fit statistics
# Multi-event results
task$get_aar(stat_name = "CSectT") # AAR/CAAR table
# Broom-compatible tidy output
tidy.EventStudyTask(task, type = "ar")
tidy.EventStudyTask(task, type = "car")
tidy.EventStudyTask(task, type = "aar")
tidy.EventStudyTask(task, type = "model")
# CSV (one file per result type)
export_results(task, "results.csv")
# Excel (each result type on a separate sheet)
export_results(task, "results.xlsx")
# LaTeX (publication-ready tables)
export_results(task, "results.tex", which = c("model", "aar"))
# Event study plots with confidence bands
plot_event_study(task, type = "car", event_id = 1)
plot_event_study(task, type = "caar", group = "VW Group")
# Model diagnostics
plot_diagnostics(task, event_id = 1)
# CAR distribution across events
plot_car_distribution(task, by_group = TRUE)
# Raw price plots
plot_stocks(task, add_event_date = TRUE)
validate_task(task)
model_diagnostics(task)
pretrend_test(task)
| Model | Class | Description |
|---|---|---|
| Market Model | MarketModel |
OLS: $R_i = \alpha + \beta R_m + \epsilon$ |
| Market Adjusted | MarketAdjustedModel |
$AR_i = R_i - R_m$ |
| Mean Adjusted | ComparisonPeriodMeanAdjustedModel |
$AR_i = R_i - \bar{R}_i^{est}$ |
| Fama-French 3-Factor | FamaFrench3FactorModel |
$R_i - R_f = \alpha + \beta_m(R_m - R_f) + \beta_s SMB + \beta_h HML + \epsilon$ |
| Fama-French 5-Factor | FamaFrench5FactorModel |
3-factor + RMW + CMA |
| Carhart 4-Factor | Carhart4FactorModel |
3-factor + MOM |
| GARCH(1,1) | GARCHModel |
Time-varying volatility (requires rugarch) |
| BHAR | BHARModel |
Buy-and-hold: $\prod(1+R_i) - \prod(1+R_m)$ |
| Volume | VolumeModel |
Abnormal trading volume |
| Volatility | VolatilityModel |
Abnormal volatility (variance ratio) |
| Custom | CustomModel |
Extend MarketModel with custom AR logic |
| Linear Factor | LinearFactorModel |
Base class for all OLS factor models |
Factor models require a factor_tbl with Fama-French data:
factor_data <- tibble(
date = dates, # matching format as firm/index data
smb = smb_returns,
hml = hml_returns,
risk_free_rate = rf_rate # triggers automatic excess return computation
)
task <- EventStudyTask$new(firm_tbl, index_tbl, request_tbl,
factor_tbl = factor_data)
params <- ParameterSet$new(return_model = FamaFrench3FactorModel$new())
task <- run_event_study(task, params)
| Test | Class | Description |
|---|---|---|
| AR T-Test | ARTTest |
$t = AR_{i,t} / \sigma_i$ |
| CAR T-Test | CARTTest |
$t = CAR_i / (\sqrt{L} \cdot \sigma_i)$ |
| BHAR T-Test | BHARTTest |
t-test on buy-and-hold abnormal returns |
| Test | Class | Description |
|---|---|---|
| Cross-Sectional T | CSectTTest |
Standard cross-sectional t-test |
| Patell Z | PatellZTest |
Standardized residual test (Patell 1976) |
| BMP Test | BMPTest |
Standardized cross-sectional (Boehmer et al. 1991) |
| Sign Test | SignTest |
Proportion of positive ARs vs 50% |
| Generalized Sign | GeneralizedSignTest |
Adjusted for asymmetry (Cowan 1992) |
| Rank Test | RankTest |
Non-parametric rank test (Corrado 1989) |
| Calendar-Time Portfolio | CalendarTimePortfolioTest |
Portfolio approach for clustered events |
Explain why some firms are affected more than others:
firm_chars <- tibble(
event_id = 1:4,
log_market_cap = c(10, 11, 12, 10.5),
leverage = c(0.3, 0.5, 0.2, 0.4)
)
result <- cross_sectional_regression(
task, formula = ~ log_market_cap + leverage,
data = firm_chars
)
print(result)
# Group comparisons
car_by_group(task)
car_quantiles(task)
For high-frequency studies around earnings announcements, Fed decisions, etc.:
task <- IntradayEventStudyTask$new(
firm_intraday_tbl, # symbol, timestamp (POSIXct), price
index_intraday_tbl,
request_tbl # event_timestamp, windows in observation counts
)
task <- prepare_intraday_event_study(task, ParameterSet$new())
task <- fit_model(task, ParameterSet$new())
Non-parametric significance test (Rinaudo & Saha 2014): compares event-day CARs to the empirical distribution of estimation-day CARs to build confidence bands without distributional assumptions.
results <- nonparametric_intraday_test(
estimation_window = est_data, # day, time, abnormalReturn
event_window = event_data, # time, abnormalReturn
event_times = c("10:00", "14:30"),
p = 0.05
)
See vignette("intraday-event-study") for details.
For difference-in-differences style analysis with staggered treatment:
task <- PanelEventStudyTask$new(
panel_data,
unit_id = "firm_id",
time_id = "year",
outcome = "revenue",
treatment = "policy_adopted",
treatment_time = "adoption_year"
)
# Dynamic TWFE with event study plot
task <- estimate_panel_event_study(task, method = "dynamic_twfe",
leads = 5, lags = 5)
plot_panel_event_study(task)
# Sun & Abraham for staggered treatment
task <- estimate_panel_event_study(task, method = "sun_abraham",
leads = 5, lags = 5)
MyModel <- R6::R6Class("MyModel",
inherit = ModelBase,
public = list(
model_name = "MyModel",
fit = function(data_tbl) {
# Estimate on estimation window, set private$.is_fitted = TRUE
# Populate private$.statistics$sigma and $degree_of_freedom
},
abnormal_returns = function(data_tbl) {
# Return data_tbl with an abnormal_returns column
}
)
)
MyTest <- R6::R6Class("MyTest",
inherit = TestStatisticBase,
public = list(
name = "MyTest",
compute = function(data_tbl, model) {
# Return a tibble with your test results
}
)
)
multi_tests <- MultiEventStatisticsSet$new()
multi_tests$add_test(MyTest$new())
See vignette("custom-models") and vignette("custom-test-statistics") for detailed tutorials.
| Vignette | Topic |
|---|---|
ai-advisor |
Grounded AI advisor two-layer walkthrough on the bundled dieselgate dataset |
introduction |
Package overview with Dieselgate example |
custom-models |
Creating and using custom return models |
custom-test-statistics |
Creating custom test statistics |
result-extraction |
Extracting and exporting results |
factor-models-bhar |
Factor models (FF3, FF5, Carhart), GARCH, and BHAR |
cross-sectional-analysis |
Cross-sectional regression, group comparisons, CAR quantiles |
diagnostics-validation |
Data validation, model diagnostics, pre-trend testing |
volume-volatility-event-study |
Volume and volatility event studies |
intraday-event-study |
Non-parametric intraday event studies |
panel-event-study |
Panel DiD event studies |
Advisor Pro is a planned premium add-on that will provide retrieval-grounded,
evidence-backed AI advice powered by a curated academic knowledge base. It will
integrate seamlessly with the existing es_advise() / es_diagnostics() pipeline.
Planned features:
es_diagnostics() outputAdvice objects with traceable evidence chainsCurrent free tier already includes offline KB-based advice via
recommend_stat() and flag_robustness() — no API key required.
Join the waitlist: https://github.com/sipemu/eventstudy#advisor-pro-waitlist
Opt-in footer reminder (silent by default):
options(eventstudy.advisor_pro_footer = TRUE)
# Appends a static waitlist URL to advice print output — no network call.
See ?advisor_pro for details.
AGPL-3