Skip to contents

SYNTHETIC DATA NOTICE. Every observation in this example is generated by simulate_event_study() with a fixed seed. No real M&A transactions or stock prices are used. The example exists to teach event-study methodology and to demonstrate statistical power – not to describe any actual deal.

Abstract

Merger and acquisition announcements are a classic event-study application: target shareholders typically earn large positive abnormal returns on the announcement day. But how large must the true effect be, and how many deals must you observe, before a test can reliably detect it? This example uses simulate_event_study() to answer that question directly, sweeping the true abnormal-return magnitude and reading off the statistical power of the cross-sectional test.

For the underlying model and test, see Return Models and Test Statistics.

Research question

For a synthetic M&A deal sample, what minimum true abnormal return is needed for the cross-sectional test to achieve roughly 80% power at the 5% significance level, holding the number of events fixed?

Simulated data

simulate_event_study() draws firm returns from a market model with known parameters, embeds a known event effect of a chosen magnitude, and runs the event study n_simulations times, returning an es_simulation object. Its components are the empirical power, a per-day rejection_by_day tibble, the vector of realised test_stats, and the params used.

library(EventStudy)

sim_result <- simulate_event_study(
  n_events         = 15,
  event_window     = c(-5, 5),
  abnormal_return  = 0.01,   # true event-day effect embedded in the DGP
  n_simulations    = 200,
  seed             = 42
)

str(sim_result, max.level = 1)
#> List of 4
#>  $ power           : num 0.96
#>  $ rejection_by_day: tibble [11 × 2] (S3: tbl_df/tbl/data.frame)
#>  $ test_stats      : num [1:200] 5.13 3.55 5.35 3.33 5.29 ...
#>  $ params          :List of 8
#>  - attr(*, "class")= chr "es_simulation"
cat("Empirical power at abnormal_return = 0.01:", sim_result$power, "\n")
#> Empirical power at abnormal_return = 0.01: 0.96

Power sweep

The primary result is a sweep over the true abnormal-return magnitude. For each value we simulate afresh (fixed seed for reproducibility) and record the empirical power – the fraction of the n_simulations runs in which the test correctly rejected the null on the event day.

# Extraction approach: simulate_event_study() returns an `es_simulation`
# object whose `$power` field is exactly the detection rate we want. We map
# over a grid of true effects and read $power from each run.
ar_grid <- c(0.001, 0.002, 0.004, 0.006, 0.010)

power_tbl <- purrr::map_dfr(ar_grid, function(ar) {
  sim <- simulate_event_study(
    n_events        = 15,
    event_window    = c(-5, 5),
    abnormal_return = ar,
    n_simulations   = 200,
    seed            = 42
  )
  tibble::tibble(true_abnormal_return = ar, power = sim$power)
})

knitr::kable(
  power_tbl, digits = 4,
  caption = "Detection power by true abnormal return (N = 15, cross-sectional test, alpha = 0.05)"
)
Detection power by true abnormal return (N = 15, cross-sectional test, alpha = 0.05)
true_abnormal_return power
0.001 0.035
0.002 0.100
0.004 0.270
0.006 0.570
0.010 0.960

Results plot

The es_simulation object has no built-in event-study plot (it is a simulation summary, not a fitted task), so we render the power curve directly with plotly. The dashed line marks the conventional 80% power target.

plotly::plot_ly(
  power_tbl,
  x = ~true_abnormal_return, y = ~power,
  type = "scatter", mode = "lines+markers",
  line = list(color = "#20c997", width = 3),
  marker = list(color = "#20c997", size = 8)
) |>
  plotly::add_segments(
    x = min(ar_grid), xend = max(ar_grid), y = 0.8, yend = 0.8,
    line = list(color = "grey", dash = "dash"), showlegend = FALSE,
    inherit = FALSE
  ) |>
  plotly::layout(
    title = "Power curve: detection rate vs true abnormal return (synthetic)",
    xaxis = list(title = "True abnormal return"),
    yaxis = list(title = "Power", range = c(0, 1))
  )

Power analysis interpretation

The power curve rises monotonically with the true effect size. At a true abnormal return of 0.1% the test is essentially blind (power near the nominal 5% false-positive floor); by 1% the test detects the effect in the large majority of samples. The practical reading: if a researcher expects M&A deal announcements to move target prices by the customary several percent, a sample of even fifteen deals gives ample power at conventional significance levels. Weak or diffuse effects, by contrast, demand far larger samples – the familiar power-vs-effect-size tradeoff (MacKinlay 1997).

Note: power estimates above are Monte Carlo averages from 200 simulation runs. At p ≈ 0.50 the sampling error is roughly ±3–5 percentage points (95% interval), so small differences between adjacent grid points are within simulation noise and should not be over-interpreted.

Design implications

Power is jointly determined by the true effect size, the number of events, the event-window width, and the estimation-window length (which sharpens the market-model fit and shrinks the abnormal-return standard error). Widening the event window dilutes a concentrated announcement effect; lengthening the estimation window tightens the benchmark. For a full treatment see the companion Monte Carlo Power Analysis vignette and simulate_event_study().

Diagnostics note

On real (non-simulated) M&A samples, always check model-fit quality with es_diagnostics(): thin trading around small targets and confounding news in the window are common threats that this synthetic ideal-conditions example deliberately excludes.

Further reading

Session info

#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.4 LTS
#> 
#> Matrix products: default
#> BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=C.UTF-8       LC_NUMERIC=C           LC_TIME=C.UTF-8       
#>  [4] LC_COLLATE=C.UTF-8     LC_MONETARY=C.UTF-8    LC_MESSAGES=C.UTF-8   
#>  [7] LC_PAPER=C.UTF-8       LC_NAME=C              LC_ADDRESS=C          
#> [10] LC_TELEPHONE=C         LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C   
#> 
#> time zone: UTC
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] EventStudy_0.62.0
#> 
#> loaded via a namespace (and not attached):
#>  [1] gtable_0.3.6         jsonlite_2.0.0       dplyr_1.2.1         
#>  [4] compiler_4.6.1       tidyselect_1.2.1     stringr_1.6.0       
#>  [7] tidyr_1.3.2          jquerylib_0.1.4      systemfonts_1.3.2   
#> [10] scales_1.4.0         textshaping_1.0.5    yaml_2.3.12         
#> [13] fastmap_1.2.0        ggplot2_4.0.3        R6_2.6.1            
#> [16] generics_0.1.4       distributional_0.8.1 knitr_1.51          
#> [19] htmlwidgets_1.6.4    tibble_3.3.1         desc_1.4.3          
#> [22] RColorBrewer_1.1-3   bslib_0.12.0         pillar_1.11.1       
#> [25] rlang_1.3.0          stringi_1.8.9        cachem_1.1.0        
#> [28] xfun_0.60            S7_0.2.2             fs_2.1.0            
#> [31] sass_0.4.10          otel_0.2.0           viridisLite_0.4.3   
#> [34] plotly_4.12.1        cli_3.6.6            withr_3.0.3         
#> [37] pkgdown_2.2.1        magrittr_2.0.5       crosstalk_1.2.2     
#> [40] digest_0.6.39        grid_4.6.1           lifecycle_1.0.5     
#> [43] vctrs_0.7.3          data.table_1.18.6.1  evaluate_1.0.5      
#> [46] glue_1.8.1           farver_2.1.2         ragg_1.5.2          
#> [49] purrr_1.2.2          httr_1.4.9           rmarkdown_2.32      
#> [52] tools_4.6.1          pkgconfig_2.0.3      htmltools_0.5.9
MacKinlay, A. Craig. 1997. “Event Studies in Economics and Finance.” Journal of Economic Literature 35 (1): 13–39.