Every event-study statistic embeds convention choices – the return
type, the forecast-error correction, the degrees of freedom, the p-value
sidedness, the Patell denominator, and so on. Two implementations of
“the same” test can differ in the last digits (or more) purely because
they made different, equally defensible choices. This vignette
documents, per return model and per test statistic, exactly which
convention EventStudy uses and cites the published source for each. It
is the reference that a golden-value regression test is pinned against:
a documented convention here matches the assumption annotated in
tests/testthat/test_golden_values.R.
Each model and statistic was audited against its published formula. Findings are classified with a strict two-bucket policy:
When a discrepancy is ambiguous it is treated as a convention (documented, not changed), because silently altering a valid-input result is the higher-risk error.
Tolerance policy for the golden tests: absolute 1e-10
for exact algebraic identities, absolute 1e-8 as the
general default, and relative 1e-6 for any
cross-implementation comparison. Each pinned assertion states its
provenance, the conventions it assumes, and its tolerance rationale
inline.
The Market Model regresses firm returns on index returns over the estimation window by OLS and defines the abnormal return as the residual in the event window.
| Convention | Choice | Note |
|---|---|---|
| Return type | Arithmetic (simple) returns | The pipeline’s return-calculation strategy is orthogonal; the model
consumes whatever firm_returns / index_returns
contain. |
| Estimation | OLS: firm_returns ~ index_returns |
alpha, beta from lm(). |
| Abnormal return | AR = R - (alpha + beta * Rm) |
MacKinlay (1997) eq. (5). |
| Residual sigma | Residual standard error from summary(lm) |
Uses df = m - 2 (intercept + slope), m =
COMPLETE-PAIR estimation observations. |
| Degrees of freedom | m - 2 |
m = estimation observations with both
firm_returns and index_returns finite. |
| Minimum observations | n_valid >= n_params + 1 = 3 |
Fewer valid observations route through the degenerate-input contract
(NA + one warning, or error in strict mode) rather than fitting an
unstable/undefined sigma (needs
df >= 1). |
| Short-window advisory | One plain warning when 2 < n_valid < 30 |
Advisory only (not a degenerate/contract condition) – the model
still fits; 30 is the same recommended minimum as
validate_task()’s default. |
statistics$n_params |
2 (intercept + slope) |
Consumed by PatellZTest’s Q_i adjustment
(see below). |
| AR t-statistic | AR / sigma |
Per-day, constant-sigma approximation. |
| CAR t-statistic | CAR / (sqrt(L) * sigma) |
L-day cumulative, constant-sigma approximation,
MacKinlay (1997). |
| Forecast-error correction | sigma * sqrt(1 + 1/m + (Rm_evt - mean(Rm_est))^2 / SS_market) |
Patell/MacKinlay prediction-error variance;
mean(Rm_est) and SS_market (the
estimation-window sum of squares of the index) are computed over
COMPLETE PAIRS ONLY, matching exactly the rows lm() used
(2026-09-24 re-evaluation item A6: previously included incomplete-pair
rows, biasing the mean/SS). Falls back to the constant-mean form
sigma * sqrt(1 + 1/m) when the index is constant. |
| p-value sidedness | Two-sided by default | confidence_type = "two-sided". |
Audit result: verified against MacKinlay (1997); two
silent-wrong defects fixed 2026-09-24 (items A6). The core
OLS/AR/CAR formulas match MacKinlay. The 2026-09-24 re-evaluation found
the insufficient-observations guard used a flat
n_valid < 2 threshold (too permissive for a 2-parameter
OLS fit to be meaningful) and the forecast-error correction’s
mean(Rm_est)/SS_market included estimation
rows where firm_returns was NA but
index_returns was not – rows lm() silently
drops via na.omit() but which the correction formula still
summed over, biasing the correction away from the fitted model’s own
estimation sample. Both are fixed; pinned in
test_golden_values.R and
test_reeval_statistical_fixes.R against an independent
closed-form lm() derivation restricted to complete pairs,
at absolute tolerance 1e-10/1e-12.
Citation: MacKinlay, A. C. (1997). Event Studies in Economics and Finance. Journal of Economic Literature, 35(1), 13–39.
The Market Adjusted (index) model treats the market index itself as
the expected return: it fixes alpha = 0 and
beta = 1 rather than estimating them, so the abnormal
return is simply the firm return minus the index return. It is the
restricted market model of MacKinlay (1997) sec. 4.4.2.
| Convention | Choice | Note |
|---|---|---|
| Return type | Whatever firm_returns / index_returns
contain |
Return-calculation strategy is orthogonal. |
| Estimation | None – alpha = 0, beta = 1 fixed |
No OLS parameters estimated. |
| Abnormal return | AR = R - Rm |
Firm return minus index return. |
| Residual sigma | sd(firm - index) over the estimation window |
Sample standard deviation of the market-adjusted residual series. |
| Degrees of freedom | m - 1 |
One parameter consumed by the residual mean in sd();
m = estimation observations. |
statistics$n_params |
0 |
Nothing is estimated (alpha/beta are FIXED at 0/1, not fitted). |
| Forecast-error correction | forecast_error_corrected_sigma == sigma (correction
factor exactly 1) |
No regression, no leverage term, and – since nothing is estimated – no prediction-error inflation either (2026-09-24 re-evaluation item A8). |
| p-value sidedness | Two-sided by default | – |
Audit result: one silent-wrong defect fixed 2026-09-24 (item
A8). The subtraction AR = R - Rm matches the
restricted market model. The 2026-09-24 re-evaluation found the
forecast-error correction wrongly reused the constant-MEAN formula
sigma * sqrt(1 + 1/m) – which is only valid when ONE
parameter (a mean) is estimated from the estimation window, as in the
Comparison Period Mean Adjusted Model below. MarketAdjustedModel
estimates NOTHING (alpha/beta are fixed, not fitted), so its correction
factor is exactly 1:
forecast_error_corrected_sigma == sigma. Re-pinned in
test_golden_values.R and locked in
test_reeval_statistical_fixes.R at absolute tolerance
1e-10/1e-12.
Citation: MacKinlay, A. C. (1997). Event Studies in Economics and Finance. Journal of Economic Literature, 35(1), 13–39.
The Comparison Period Mean (constant-mean-return) model uses the firm’s own average return over the estimation window as the expected return. It is the constant-mean-return model of Brown & Warner (1985), also described in MacKinlay (1997) sec. 4.4.1.
| Convention | Choice | Note |
|---|---|---|
| Return type | Whatever firm_returns contains |
Index returns are unused by this model. |
| Estimation | mean(firm_returns) over the estimation window |
One parameter (the mean) estimated. |
| Abnormal return | AR = R - mean(R_est) |
Firm return minus estimation-window mean. |
| Residual sigma | sd(firm - mean) over the estimation window |
Sample standard deviation of the demeaned residuals. |
| Degrees of freedom | m - 1 |
The mean consumes one degree of freedom. |
statistics$n_params |
1 |
One parameter estimated (the comparison-period mean). |
| Forecast-error correction | sigma * sqrt(1 + 1/m) |
Constant-mean form. Uses the finite value count (NA rows do not
inflate m). Unlike MarketAdjustedModel (A8), this model
DOES estimate one parameter, so the constant-mean correction is the
correct form here. |
| p-value sidedness | Two-sided by default | – |
Audit result: verified, no change. AR, sigma, df,
and FEC match the constant-mean-return model. Pinned in
test_golden_values.R against a closed-form subtraction
against the fixed estimation mean at absolute tolerance
1e-10.
Citation: Brown, S. J., & Warner, J. B. (1985). Using daily stock returns: The case of event studies. Journal of Financial Economics, 14(1), 3–31. MacKinlay (1997).
Regresses firm excess returns on the three Fama-French factors –
market excess (market_excess), size (smb), and
value (hml) – by OLS over the estimation window; the
abnormal return is the residual in the event window. All factor models
share the LinearFactorModel OLS core.
| Convention | Choice | Note |
|---|---|---|
| Return type | Excess returns (excess_return = firm minus
risk-free) |
Regressed on market_excess + smb + hml. |
| Estimation | OLS via
lm(excess_return ~ market_excess + smb + hml) |
k = 3 factors plus intercept. |
| Abnormal return | AR = excess_return - predicted |
predicted from the fitted OLS coefficients. |
| Residual sigma | Residual standard error from summary(lm) |
– |
| Degrees of freedom | m - (k + 1) = m - 4 |
Intercept plus three factor slopes. |
| Minimum observations | n_valid >= n_params + 1 = 5 |
n_params = length(all.vars(formula)) = 4 (response + 3
factors, i.e. intercept + 3 slopes); fewer valid observations route
through the degenerate-input contract (2026-09-24 item A6). |
| Short-window advisory | One plain warning when 4 < n_valid < 30 |
Same advisory convention as the Market Model. |
statistics$n_params |
4 |
Consumed by PatellZTest’s Q_i
adjustment. |
| Forecast-error correction | sigma * sqrt(1 + h_t),
h_t = x_t' (X'X)^{-1} x_t |
Full multi-factor hat-value (leverage) form using the estimation design matrix. |
| p-value sidedness | Two-sided by default | – |
Audit result: verified, no change (guard strengthened
2026-09-24, item A6). The OLS fit and hat-value FEC match
Fama-French (1993). Pinned in test_golden_values.R against
an independent lm()/predict() derivation at
absolute tolerance 1e-10.
Citation: Fama, E. F., & French, K. R. (1993). Common risk factors in the returns on stocks and bonds. Journal of Financial Economics, 33(1), 3–56.
Extends the three-factor model with profitability (rmw)
and investment (cma) factors – Fama & French (2015).
Same OLS core, five factors.
| Convention | Choice | Note |
|---|---|---|
| Return type | Excess returns (excess_return) |
Regressed on
market_excess + smb + hml + rmw + cma. |
| Estimation | OLS on five factors plus intercept | k = 5. |
| Abnormal return | AR = excess_return - predicted |
– |
| Residual sigma | Residual standard error | – |
| Degrees of freedom | m - (k + 1) = m - 6 |
– |
| Minimum observations | n_valid >= n_params + 1 = 7 |
n_params = 6 (intercept + 5 factors); 2026-09-24 item
A6. |
statistics$n_params |
6 |
Consumed by PatellZTest’s Q_i
adjustment. |
| Forecast-error correction | sigma * sqrt(1 + h_t) |
Multi-factor hat-value form. |
| p-value sidedness | Two-sided by default | – |
Audit result: verified, no change (guard strengthened
2026-09-24, item A6). Pinned in
test_golden_values.R against an independent
lm()/predict() derivation at absolute
tolerance 1e-10.
Citation: Fama, E. F., & French, K. R. (2015). A five-factor asset pricing model. Journal of Financial Economics, 116(1), 1–22.
Extends the three-factor model with a momentum factor
(mom) – Carhart (1997). Same OLS core, four factors.
| Convention | Choice | Note |
|---|---|---|
| Return type | Excess returns (excess_return) |
Regressed on market_excess + smb + hml + mom. |
| Estimation | OLS on four factors plus intercept | k = 4. |
| Abnormal return | AR = excess_return - predicted |
– |
| Residual sigma | Residual standard error | – |
| Degrees of freedom | m - (k + 1) = m - 5 |
– |
| Minimum observations | n_valid >= n_params + 1 = 6 |
n_params = 5 (intercept + 4 factors); 2026-09-24 item
A6. |
statistics$n_params |
5 |
Consumed by PatellZTest’s Q_i
adjustment. |
| Forecast-error correction | sigma * sqrt(1 + h_t) |
Multi-factor hat-value form. |
| p-value sidedness | Two-sided by default | – |
Audit result: verified, no change (guard strengthened
2026-09-24, item A6). Pinned in
test_golden_values.R against an independent
lm()/predict() derivation at absolute
tolerance 1e-10.
Citation: Carhart, M. M. (1997). On persistence in mutual fund performance. Journal of Finance, 52(1), 57–82.
The Custom Model inherits the Market Model’s OLS estimation but
defines the abnormal return as
firm_returns - predict(user_model), plus an optional
event-date adjustment loss_market_cap added only on the
event date. There is no single published closed-form here – the
convention is deliberately “whatever the supplied model predicts.”
| Convention | Choice | Note |
|---|---|---|
| Return type | Whatever the supplied model consumes | User-defined. |
| Estimation | Inherited from the Market Model (OLS) | The fitted object is user-supplied via the pipeline. |
| Abnormal return | AR = R - predict(model) (+loss_market_cap
on the event date) |
The event-date term lets a user inject a known shock. |
| Residual sigma / df | Inherited from the underlying OLS fit (df = m - 2) |
– |
| p-value sidedness | Two-sided by default | – |
Audit result: documented convention (no closed-form pin
possible). With loss_market_cap = 0 the Custom
Model reduces exactly to the Market Model; that identity is what
test_golden_values.R pins (absolute 1e-10). A
general user-supplied prediction has no published reference value to pin
against.
Citation: Convention is user-defined; the reduction case cites MacKinlay (1997).
The Buy-and-Hold Abnormal Return (BHAR) model is for long-horizon studies where compounding matters. Instead of summing daily abnormal returns it compounds firm and benchmark returns separately over the event window and takes the difference.
| Convention | Choice | Note |
|---|---|---|
| Return type | Arithmetic returns compounded multiplicatively | cumprod(1 + R). |
| Estimation | None estimated for the AR itself; the benchmark is the index | sigma is estimated from the estimation window for the t-test. |
| Abnormal return | BHAR = cumprod(1 + firm) - cumprod(1 + index) |
Barber & Lyon (1997) buy-and-hold definition; compounded within each event window separately. |
| Residual sigma | sd(firm - index) over the estimation window |
Sample SD of the one-period market-adjusted residual series. |
| Degrees of freedom | m - 1 |
Finite-pair count via .finite_residual_df() (NA rows do
not inflate m). |
| BHAR standard error | sigma * sqrt(n) |
n is the day index within the event window – the
random-walk sqrt-of-horizon scaling of Lyon, Barber & Tsai
(1999). |
| BHAR t-statistic | BHAR / (sigma * sqrt(n)) |
Returns NA when sigma == 0 (degenerate guard), never
Inf/NaN. |
| p-value sidedness | Two-sided by default | – |
Audit result: verified, no change. The compounded
BHAR, the sigma * sqrt(n) scaling, and the constant-mean
forecast-error correction match Barber-Lyon (1997) / Lyon-Barber-Tsai
(1999). Pinned in test_golden_values.R against an
independent closed-form compounding at absolute tolerance
1e-10.
Citation: Barber, B. M., & Lyon, J. D. (1997). Detecting long-run abnormal stock returns: The empirical power and specification of test statistics. Journal of Financial Economics, 43(3), 341–372. Lyon, J. D., Barber, B. M., & Tsai, C.-L. (1999). Improved methods for tests of long-run abnormal stock returns. Journal of Finance, 54(1), 165–201.
The Volume model runs an event study on trading volume rather than
returns. Expected volume is the estimation-window mean of (optionally
log-transformed) volume, and abnormal volume is the deviation from that
mean. It writes abnormal volume into the abnormal_returns
column so the standard test statistics apply.
| Convention | Choice | Note |
|---|---|---|
| Measure | log(firm_volume + 1) by default
(log_transform = TRUE) |
The +1 avoids log(0); set
log_transform = FALSE for raw volume. |
| Estimation | mean(log(volume + 1)) over the estimation window |
One parameter (the mean). |
| Abnormal measure | AR = log(volume + 1) - expected |
Deviation from the estimation-window log-mean. |
| Residual sigma | sd(residuals) over the estimation window |
– |
| Degrees of freedom | m - 1 |
Finite value count (NA/non-finite rows excluded). |
| p-value sidedness | Two-sided by default | – |
Audit result: verified, no change. The log-mean
expected volume, the deviation-from-mean abnormal volume, and
df = m - 1 match the standard abnormal-trading-volume
convention. Pinned in test_golden_values.R against a direct
log-mean subtraction at absolute tolerance 1e-10.
Citation: Convention follows the abnormal-trading-volume literature (e.g. Campbell & Wasley 1996, log relative volume); the mean-adjusted form is the volume analogue of the constant-mean-return model of Brown & Warner (1985).
The Volatility model runs an event study on return variance. Expected
variance is the estimation-window sample variance, and abnormal
volatility is the ratio of each event-window squared return to that
variance, minus one (so zero means “as expected”). It writes the measure
into abnormal_returns for compatibility.
| Convention | Choice | Note |
|---|---|---|
| Measure | Squared return relative to estimation variance | – |
| Estimation | est_var = var(estimation firm_returns) |
Sample variance, df = m - 1. |
| Abnormal measure | AR = firm_returns^2 / est_var - 1 |
Zero when the squared return equals the expected variance. |
| Residual sigma | sd(residuals) of the ratio series over the estimation
window |
– |
| Degrees of freedom | m - 1 |
Finite value count. |
| p-value sidedness | Two-sided by default | – |
Audit result: verified, no change. The
ratio-to-expected-variance abnormal measure and df = m - 1
match the abnormal-volatility convention. Pinned in
test_golden_values.R against a direct ratio computation at
absolute tolerance 1e-10.
Citation: The squared-return / expected-variance ratio follows the event-induced-variance literature (Beaver 1968; Patell & Wolfson 1979), adapted to the constant-variance benchmark of the estimation window.
The Rolling-Window model captures parameter instability by estimating the market model over a rolling OLS window and using the LAST window’s parameters to predict the event window. This yields a time-varying beta without a full GARCH model.
| Convention | Choice | Note |
|---|---|---|
| Estimation | Rolling OLS firm ~ index over windows of
window_size (default 60), requiring min_obs
(default 30) finite pairs |
Last window’s alpha, beta used for
prediction. |
| Effective window | ws = min(window_size, n_est) |
When n_est <= window_size there is a single window
equal to the full sample; n_est is the estimation window’s
ROW span (not a valid-pair count). |
| Within-window pairs | Each ws-row window restricts
x_bar/y_bar/ss_xx/ss_xy/residuals/sigma
to COMPLETE PAIRS (both firm and index finite) |
2026-09-24 re-evaluation item A1: previously
x_bar/ss_xx were computed from ALL index
values in the window, including rows where the firm return was NA – if
those NA-firm rows are not missing-at-random in the index (e.g. an index
outlier co-occurring with a missing firm observation), this silently
biases beta away from what lm() on the complete-pair subset
would produce. |
| Abnormal return | AR = firm - (alpha_last + beta_last * index) |
Uses the most recent (last) rolling parameters. |
| Residual sigma | Last-window residual SE,
denom = max(n_complete_last - 2, 1) |
n_complete_last = complete pairs in the LAST window
(not a freshly recomputed valid-count that could disagree with the
actual last-window row span). |
| Degrees of freedom | max(n_complete_last - 2, 1) |
fit() and calculate_statistics() share the
SAME complete-pair count (stored in
private$.rolling_params$n_completes), so the reported df
always matches the sigma that was actually estimated. |
statistics$n_params |
2 (alpha, beta) |
Consumed by PatellZTest’s Q_i
adjustment. |
| p-value sidedness | Two-sided by default | – |
Audit result: one silent-wrong defect fixed 2026-09-24 (item
A1). The rolling OLS, last-window prediction, and
df = n_complete - 2 are internally consistent once
restricted to complete pairs. Pinned in
test_golden_values.R on a 30-observation fixture where
ws = min(60, 30) = 30 collapses to a single closed-form
OLS, and in test_reeval_statistical_fixes.R on a 150-day
window with firm-return NAs co-occurring with index outliers every 3rd
day (beta, alpha, sigma, and df all match lm() on the
complete-pair subset within 1e-10).
Citation: Rolling-window / time-varying market-model estimation is standard practice for capturing beta instability (e.g. Fama & MacBeth 1973 rolling regressions); the underlying market model is MacKinlay (1997).
The GARCH model fits a GARCH(1,1) mean-variance model to firm returns with the index return as an external regressor in the mean equation (via the optional rugarch package). The abnormal return is the mean-equation residual.
| Convention | Choice | Note |
|---|---|---|
| Backend | rugarch::ugarchfit, sGARCH,
garchOrder default c(1, 1), normal errors |
Optional dependency, guarded by requireNamespace. |
| Mean equation | firm = mu + mxreg1 * index + e_t |
Index return as an external regressor. |
| Abnormal return | AR = firm - (mu + mxreg1 * index) |
The fitted mean-equation residual. |
| Residual sigma | Mean conditional sigma from the GARCH fit | Falls back to sd(residuals) if non-finite. |
| Degrees of freedom | max(length(cond_sigma) - length(coefs), 1) |
– |
| p-value sidedness | Two-sided by default | – |
Audit result: verified, no change (identity-pinned).
Because rugarch is an optional dependency and its fitted coefficients
vary across package and solver versions, pinning a raw fitted constant
would be fragile (threat T-26-02). The documented golden-value choice is
therefore to pin the STABLE ALGEBRAIC IDENTITY
AR = firm - (mu + mxreg1 * index), which holds for any
fitted coefficients, in test_golden_values.R guarded by
skip_if_not_installed("rugarch") – locking the AR formula
without a version-fragile number.
Citation: Engle, R. F. (1982). Autoregressive conditional heteroscedasticity with estimates of the variance of United Kingdom inflation. Econometrica, 50(4), 987–1007. Bollerslev, T. (1986). Generalized autoregressive conditional heteroskedasticity. Journal of Econometrics, 31(3), 307–327.
The DCC-GARCH model (via the optional rmgarch
package) fits a bivariate Dynamic Conditional Correlation GARCH to firm
and market returns, yielding a time-varying beta
beta_t = Cov(firm, mkt)_t / Var(mkt)_t. The last
conditional beta predicts the event window.
| Convention | Choice | Note |
|---|---|---|
| Backend | rmgarch::dccfit, univariate sGARCH(1,1),
DCC order default c(1, 1), multivariate normal |
Optional dependency, guarded by requireNamespace. |
| Time-varying beta | beta_t = H[firm, mkt]_t / H[mkt, mkt]_t |
From the conditional covariance array; non-finite / zero-variance entries yield NA and fall back to the last valid beta. |
| Intercept | alpha_last = mean(firm - beta_last * index) over the
estimation window |
Mean-equation intercept implied by the last beta. |
| Abnormal return | AR = firm - (alpha_last + beta_last * index) |
Uses the last conditional beta. |
| Residual sigma | mean(conditional firm sigma) |
– |
| Degrees of freedom | max(n_t - 4, 1) |
– |
| p-value sidedness | Two-sided by default | – |
Audit result: verified, no change (identity-pinned).
As with GARCH, rmgarch is optional and non-deterministic across
versions, so the documented golden-value choice is to pin the stable
identity AR = firm - (alpha_last + beta_last * index) in
test_golden_values.R guarded by
skip_if_not_installed("rmgarch") /
("rugarch"), rather than a fragile fitted constant.
Citation: Bollerslev, T. (1990). Modelling the coherence in short-run nominal exchange rates: A multivariate generalized ARCH model. Review of Economics and Statistics, 72(3), 498–505. Engle, R. F. (2002). Dynamic conditional correlation. Journal of Business & Economic Statistics, 20(3), 339–350.
Every CAR-based multi-event statistic (CSectTTest,
PatellZTest, SignTest,
GeneralizedSignTest, BMPTest,
KolariPynnonenTest) shares one exclusion rule, implemented
once in R/contract.R
(.exclude_all_na_events()) and applied centrally by
calculate_statistics() before any configured statistic runs
(so N statistics in one group produce AT MOST ONE warning, not N), and
defensively again at the top of each compute() for
direct/standalone calls: an event with zero finite event-window
abnormal returns is EXCLUDED from the group entirely – not
NA-propagated and coalesced to 0 – because cumulating a fully-degenerate
event’s abnormal returns as 0 would silently understate the true
cross-sectional dispersion (an entirely different failure mode from a
normal 0 abnormal return). Excluded events whose model already emitted
the one contract warning guaranteed by CONTRACT-04 (an unfitted
ModelBase instance) are dropped SILENTLY; any other
excluded event is reported once, naming the event_id(s)
(strict mode errors instead). This exclusion leaves the pre-existing
STATS-03 convention for a PARTIAL gap unchanged: an event with at least
one finite event-window abnormal return, but a missing value on some
individual day, still contributes 0 to THAT EVENT’s own CAR on the
missing day(s) via coalesce(abnormal_returns, 0).
Documented above with the Market Model, since the tracer anchors them
together. The AR t-test divides each event-day abnormal return by the
model sigma; the CAR t-test divides the cumulative abnormal return by
sqrt(L) * sigma. Both are distributed as Student’s t with
the model’s residual degrees of freedom and are two-sided by default.
Every return model also computes
forecast_error_corrected_sigma_car (the FEC’s per-event
covariance/hat-value term), but
ARTTest/CARTTest INTENTIONALLY do not use it –
they keep the simpler constant-sigma approximation (item A11, documented
convention, no formula change);
forecast_error_corrected_sigma_car exists for consumers
that need the full prediction-error covariance structure.
Citation: MacKinlay (1997).
The single-event buy-and-hold abnormal-return t-test. Documented
above with the BHAR Model, since the model and its t-test share the same
compounded definition and sigma * sqrt(n) scaling. In its
general form the statistic is
bhar_t = BHAR / (sigma * sqrt(n)), where
BHAR = cumprod(1 + firm) - cumprod(1 + index) over the
event window, sigma = sd(firm - index) over the estimation
window, and n is the day index within the event window. It
returns NA (never Inf/NaN) when sigma == 0. Two-sided by
default. Citation: Barber & Lyon (1997); Lyon, Barber &
Tsai (1999).
The cross-sectional t-test evaluates, at each relative event day, whether the average abnormal return across events is zero, using the cross-event dispersion of abnormal returns as the standard error. It is the workhorse parametric test of Brown & Warner (1985).
| Convention | Choice | Note |
|---|---|---|
| Point statistic | aar_t = sqrt(N) * aar / sd(AR) |
aar = mean AR across events; sd(AR) =
cross-event sample SD (denominator N - 1). |
| Cumulative statistic | caar_t = sqrt(N) * caar / sd(CAR) |
caar = mean of per-event cumulative ARs;
sd(CAR) = cross-event sample SD of the per-event CARs. |
| Sample size | N = number of valid (non-NA) events at that day |
NA abnormal returns are excluded from both the mean and the SD. |
| Distribution | Student’s t with N - 1 degrees of freedom |
– |
| Standardization | None – raw abnormal returns | Unlike Patell/BMP, ARs are not divided by a per-event sigma; the cross-event SD is the only scale. |
| Positive count | n_pos = sum(AR > 0) – STRICT inequality |
2026-09-24 item A9: an AR exactly 0 counts as NON-positive
(n_neg = sum(AR <= 0)), matching the
SignTest convention. |
| All-NA event exclusion | An event with ZERO finite event-window ARs is excluded from every multi-event statistic (once per group, one contract warning/error) | 2026-09-24 item A2; see “Multi-event all-NA exclusion” below. A PARTIAL gap (one missing AR mid-window in an otherwise-valid event) still contributes 0 to that event’s own CAR (STATS-03, unchanged). |
| p-value sidedness | Two-sided by default | – |
Audit result: verified against Brown & Warner (1985); two
silent-wrong defects fixed 2026-09-24 (items A2, A9). The core
sqrt(n_valid_events) * aar / sd(AR) /
caar / sd(CAR) construction matches Brown & Warner. The
2026-09-24 re-evaluation found n_pos used
>= 0 (so a zero AR silently counted as “positive”) and
that a fully-degenerate event (all event-window ARs NA, e.g. an unfitted
model) was not excluded from the cumulative CAR-based statistics – its
coalesce(AR, 0) silently understated cross-event dispersion
instead of being excluded. Both are fixed; pinned in
test_golden_values.R and
test_reeval_statistical_fixes.R against an independent
closed-form cross-sectional computation at absolute tolerance
1e-10/1e-12.
Citation: Brown, S. J., & Warner, J. B. (1985). Using daily stock returns: The case of event studies. Journal of Financial Economics, 14(1), 3–31.
The Patell (standardized-residual) test standardizes each event’s abnormal return by its own forecast-error-corrected standard deviation before aggregating, which accounts for the fact that event-window prediction errors have larger variance than estimation-window residuals. The aggregate is approximately standard normal.
| Convention | Choice | Note |
|---|---|---|
| Standardization | SAR = AR / forecast-error-corrected sigma |
Uses the per-event, per-day FEC sigma, not the raw model sigma (this is the defining feature vs. BMP). |
| Patell denominator | Q_total = sqrt(sum_i Q_i),
Q_i = (m - k) / (m - k - 2) |
m = number of FINITE estimation-window abnormal returns
for event i (2026-09-24 item A6: NOT the raw row count – NA
rows in the estimation window must not inflate m).
k = event i’s model’s
statistics$n_params (MarketModel 2, FF3 4, Carhart4 5, FF5
6, CPMA 1, MarketAdjusted 0), read explicitly rather than derived as
length(resid) - df (the pre-2026-09-24 derivation silently
produced the WRONG k whenever df already
reflected an unrelated correction, e.g. HAC). Q_i inflates
the variance of the standardized sum for the estimation-error in each
sigma. |
| Q_i fallback | Q_i = 1 when m <= k + 2 |
Insufficient observations for the adjustment; avoids a negative/undefined denominator. |
| AAR statistic | aar_z = sum_i SAR_i / Q_total |
Sum (not mean) of standardized abnormal returns over the Patell denominator. |
| CAAR statistic | caar_z = (1 / sqrt(N)) * sum_i (cumsum(SAR_i) / sqrt(n * Q_i)) |
Per-event cumulative SAR scaled by sqrt(n * Q_i) where
n is the day count, then averaged. |
| Positive count | n_pos = sum(AR > 0) – STRICT inequality |
2026-09-24 item A9: matches
SignTest/CSectTTest. |
| Degenerate-event handling | Events with all-NA FEC sigma are excluded from Q_total
(WR-01); events with ZERO finite event-window ARs are excluded from the
WHOLE statistic (2026-09-24 item A2) |
Prevents a Q_i = 1 fallback (or a coalesced-to-0 CAR
contribution) from a degenerate event distorting valid-event
z-scores. |
| N >= 2 guard | aar_z / caar_z are NA when
n_valid_events <= 1 (STATS-04) |
A single event gives a statistically invalid z; NA is returned rather than a finite-but-wrong number. |
| Distribution | Approximately N(0, 1) |
– |
| p-value sidedness | Two-sided by default | – |
Audit result: verified against Patell (1976); three
silent-wrong defects fixed 2026-09-24 (items A2, A6, A9). The
standardization by the forecast-error-corrected sigma and the
Q_total = sqrt(sum Q_i) aggregation match Patell. The
2026-09-24 re-evaluation found: (1) k was derived as
length(resid) - df instead of read from each model’s own
n_params, silently using the WRONG k whenever
df reflected an unrelated adjustment; (2) m
used the raw estimation-window row count instead of the
finite-observation count; (3) n_pos used
>= 0; and (4) an all-NA event was not excluded from the
statistic. All four are fixed; re-pinned in
test_golden_values.R and locked in
test_reeval_statistical_fixes.R against an independent
closed-form Patell computation at absolute tolerance 1e-10,
plus a guard test confirming NA for a single-event group.
Citation: Patell, J. M. (1976). Corporate forecasts of earnings per share and stock price behavior: Empirical tests. Journal of Accounting Research, 14(2), 246–276.
The Boehmer, Musumeci & Poulsen (1991) standardized cross-sectional test combines Patell-style standardization with a cross-sectional variance estimate, making it robust to event-induced increases in return variance (which the plain Patell test does not accommodate). It standardizes abnormal returns by the model sigma, then applies a cross-sectional t-test to the standardized values.
| Convention | Choice | Note |
|---|---|---|
| Standardization | SAR = AR / model sigma |
Uses the per-event MODEL sigma (contrast: Patell uses the forecast-error-corrected sigma). |
| Point statistic | bmp_t = sqrt(N) * mean(SAR) / sd(SAR) |
Cross-sectional t of the standardized abnormal returns;
sd(SAR) is the cross-event sample SD (this cross-sectional
SD is what makes BMP robust to event-induced variance). |
| Cumulative statistic | cbmp_t = sqrt(N) * mean(cumsum(SAR)) / sd(cumsum(SAR)) |
Same cross-sectional t applied to per-event cumulative SARs. |
| Join key | event_id (one sigma per event) |
A firm_symbol join would be many-to-many when a firm
recurs across events. |
| All-NA event exclusion | An event with ZERO finite event-window ARs is excluded (2026-09-24 item A2) | See “Multi-event all-NA exclusion” below. |
| Distribution | Approximately t_{N-1} |
– |
| p-value sidedness | Two-sided by default | – |
Audit result: verified, no change (all-NA exclusion added
2026-09-24, item A2; roxygen documentation corrected, item
A11). The standardization by MODEL sigma (not the
forecast-error-corrected sigma – this is the defining contrast with
PatellZTest/KolariPynnonenTest) and the
cross-sectional t sqrt(N) * mean(SAR) / sd(SAR) match
Boehmer, Musumeci & Poulsen (1991). The 2026-09-24 re-evaluation
found the roxygen documentation itself INCORRECTLY described the
standardization as using the forecast-error-corrected sigma; the formula
was always right, only the docstring was fixed (item A11, docs-only).
Pinned in test_golden_values.R against an independent
closed-form BMP computation on a fixed three-event fixture at absolute
tolerance 1e-10.
Citation: Boehmer, E., Musumeci, J., & Poulsen, A. B. (1991). Event-study methodology under conditions of event-induced variance. Journal of Financial Economics, 30(2), 253–272.
The sign test is a nonparametric test of whether positive abnormal returns occur more often than the 0.5 that would be expected by chance. Under the null the count of positive abnormal returns is Binomial(N, 0.5), approximated by a normal.
| Convention | Choice | Note |
|---|---|---|
| Positive count | n_pos = sum(AR > 0) – STRICT inequality |
A zero abnormal return counts as NON-positive
(n_neg = sum(AR <= 0)). This is the > 0
(not >= 0) convention; it is the conservative choice,
treating an exact zero as evidence against a positive effect. |
| Point statistic | sign_z = (n_pos - 0.5 N) / (0.5 sqrt(N)) |
Binomial-normal approximation with p = 0.5;
N = valid events at that day. |
| Cumulative statistic | csign_z = (n_pos_car - 0.5 N) / (0.5 sqrt(N)) |
Same form applied to the count of positive per-event CARs. |
| N >= 2 guard | sign_z / csign_z are NA when
n_valid_events < 2 (STATS-04, WR-02) |
A single observation gives a finite-but-meaningless z; NA is returned instead. |
| All-NA event exclusion | An event with ZERO finite event-window ARs is excluded (2026-09-24 item A2) | See “Multi-event all-NA exclusion” below. |
| Distribution | Approximately N(0, 1) |
– |
| p-value sidedness | Two-sided by default | – |
Audit result: verified, no change (all-NA exclusion added
2026-09-24, item A2). The
(n_pos - 0.5 N)/(0.5 sqrt(N)) statistic, the strict
> 0 positive-count convention, and the
N >= 2 guard match the standard sign test. Pinned in
test_golden_values.R against a direct binomial-normal
computation on the fixed three-event fixture at absolute tolerance
1e-10, plus a guard test confirming NA for a single-event
group.
Citation: The binomial sign test for event studies is standard; see Brown & Warner (1980, 1985) and Cowan (1992) for its use alongside the generalized form.
The generalized sign test (Cowan 1992) relaxes the ordinary sign
test’s implicit p = 0.5 by estimating the expected fraction
of positive abnormal returns from the estimation window, which corrects
for asymmetry (skewness) in the return distribution.
| Convention | Choice | Note |
|---|---|---|
| Expected proportion | p_hat = mean over EVENTS of mean(estimation AR > 0) |
Estimated PER EVENT from the estimation window, then averaged
(2026-09-24 item A4: previously grouped by firm_symbol,
which silently pooled a recurring firm’s estimation windows ACROSS its
distinct events – each event is fit independently, so this mixed
unrelated data). Replaces the ordinary test’s fixed 0.5. |
| Positive count | n_pos = sum(AR > 0) |
Same strict > 0 convention as the sign test. |
| Point statistic | gsign_z = (n_pos - N p_hat) / sqrt(N p_hat (1 - p_hat)) |
Cowan (1992) generalized sign statistic. |
| Cumulative statistic | cgsign_z = (n_pos_car - N p_hat) / sqrt(N p_hat (1 - p_hat)) |
Same form on positive per-event CAR counts. |
| Degenerate guard | NA when the denominator is non-finite or zero (p_hat in
{0, 1}) |
– |
| All-NA event exclusion | An event with ZERO finite event-window ARs is excluded (2026-09-24 item A2) | See “Multi-event all-NA exclusion” below. |
| Distribution | Approximately N(0, 1) |
– |
| p-value sidedness | Two-sided by default | – |
Audit result: one silent-wrong defect fixed 2026-09-24 (item
A4; all-NA exclusion added, item A2). The
(n_pos - N p_hat)/sqrt(N p_hat (1 - p_hat)) statistic and
the degenerate- denominator guard match Cowan (1992). The 2026-09-24
re-evaluation found p_hat was estimated by grouping the
estimation window by firm_symbol rather than
event_id: when a firm recurs across multiple events, its
estimation-window abnormal returns from DIFFERENT events were silently
pooled into one p_hat, instead of estimating one
p_hat per event and averaging those. Fixed to group by
event_id; pinned in test_golden_values.R
against an independent closed-form computation on the three-event
fixture (where the balanced estimation residuals give
p_hat = 0.5, so the generalized test reduces to the
ordinary sign test) at absolute tolerance 1e-10, and locked
in test_reeval_statistical_fixes.R (identical results
whether a firm recurs across events or every event has a distinct
firm).
Citation: Cowan, A. R. (1992). Nonparametric event study tests. Review of Quantitative Finance and Accounting, 2(4), 343–358.
The Corrado (1989) rank test is a nonparametric test that ranks each firm’s abnormal returns across the combined estimation and event windows, then tests whether the event-window ranks are unusually high or low. Ranking makes it robust to non-normality and to event-induced variance.
| Convention | Choice | Note |
|---|---|---|
| Ranking scope | Ranks computed within each EVENT over the COMBINED estimation + event window | ar_rank = rank(AR), T = combined
observations for that event (2026-09-24 item A4: previously grouped by
firm_symbol, silently pooling a recurring firm’s ranks
ACROSS its distinct events). |
| Centered rank | K = rank / (T + 1) - 0.5 |
Corrado’s mean-zero centered rank; na.last = "keep" so
missing ARs do not receive a rank. |
| Rank SD | S_rank = sd(per-day mean centered rank across ALL combined days) |
The scale is estimated from the full combined window, not just the event window. |
| Point statistic | rank_z = mean_rank_day / S_rank |
Per event day, the cross-firm mean centered rank divided by
S_rank. |
| Degenerate guard | rank_z is NA when S_rank is non-finite or
zero |
Guarded once on the scalar S_rank, then the per-day
vector is divided directly. |
| Distribution | Approximately N(0, 1) |
– |
| p-value sidedness | Two-sided by default | – |
Audit result: FIXED (broadcast bug), then verified.
The centered-rank construction and the mean_rank / S_rank
statistic match Corrado (1989). The audit found a genuine implementation
error: rank_z was computed with base
ifelse(is.finite(S_rank) & S_rank > 0, mean_rank / S_rank, NA_real_).
Because S_rank is a scalar, ifelse returned a
length-1 result (the length of its condition), collapsing the per-day
mean_rank / S_rank vector to its first element, which
dplyr::mutate then recycled across every event day – so all
event days reported the day-0 z. No published convention makes the rank
z identical on every day, so this is a bug, not a convention. It was
fixed to guard the scalar S_rank with a plain
if() and divide the vector directly; a regression test
(test_golden_values.R) locks the corrected per-day values
(+1.586, -0.952, +1.269) and asserts they are distinct,
preventing the broadcast from silently returning. The fix precedes the
golden pin.
2026-09-24 re-evaluation (item A4):
ranking/centering was also found to group by firm_symbol
instead of event_id, silently pooling a recurring firm’s
ranks across its distinct events. Fixed to group by
event_id; locked in
test_reeval_statistical_fixes.R (identical results whether
a firm recurs across events or every event has a distinct firm).
Citation: Corrado, C. J. (1989). A nonparametric test for abnormal security-price performance in event studies. Journal of Financial Economics, 23(2), 385–395.
The calendar-time portfolio approach forms, for each relative event day, an equal-weighted portfolio of all event firms’ abnormal returns and tests whether the portfolio’s mean return differs from zero. Aggregating into a portfolio naturally absorbs the cross-sectional dependence that arises when events cluster in calendar time.
| Convention | Choice | Note |
|---|---|---|
| Portfolio | Equal-weighted mean abnormal return per relative day
(aar) |
– |
| Time-series scale | ts_sd = sd(aar) over the ESTIMATION-window days |
2026-09-24 item A7 (Brown-Warner crude-dependence adjustment):
ts_sd is the standard deviation of the cross-event AAR
series computed OVER THE ESTIMATION WINDOW, not over the (much shorter,
event-specific) event window. This makes the denominator independent of
the event window and of any event-day shock magnitude. |
| Estimation-window AAR days | n_estimation_days = number of FINITE estimation-window
AAR observations |
Exposed as attr(result, "caltime_df"). |
| Point statistic | caltime_t = aar / ts_sd |
Time-series t of the portfolio AAR under
H0: E[AAR] = 0; aar is still the EVENT-window
per-day portfolio mean. |
| Cumulative statistic | ccaltime_t = caar / (ts_sd sqrt(L)) |
L = day index within the event window; random-walk
sqrt-of-horizon scaling of the cumulative portfolio return. |
| Degrees of freedom | n_estimation_days - 1 |
Brown-Warner time-series df, NOT a cross-sectional
N - 1; adjust_p_values() and
tidy() both read attr(result, "caltime_df")
rather than deriving a df of their own. |
| Positive count | n_pos = sum(AR > 0) – STRICT inequality |
2026-09-24 item A9: matches
SignTest/CSectTTest. |
| Degenerate guard | Both t’s are NA when ts_sd is non-finite/zero or
n_estimation_days < 2 |
One contract warning/error (not a silent NA) when the estimation-window AAR series cannot support a standard deviation. |
| p-value sidedness | Two-sided by default | – |
Audit result: FIXED (broadcast bug; then Brown-Warner denominator redefinition, item A7; then A9). The equal-weight portfolio construction matches the calendar-time portfolio approach. Three issues were found across two audit rounds:
caltime_t / ccaltime_t used base
ifelse(is.finite(ts_sd) & ts_sd > 0, ...) with the
scalar ts_sd as the condition, collapsing the per-day t
vectors to their first element and recycling it across every event day.
Fixed by guarding the scalar once with a plain if() and
dividing the vectors directly.ts_sd was computed from the EVENT-WINDOW
portfolio AAR series itself – the same series aar that
appears in the numerator – so a larger event-day shock inflated BOTH the
numerator and (via the window’s own dispersion) the denominator, and the
statistic was not independent of the event window’s length or contents.
This is REDEFINED (not merely a bug fix in the narrow sense) to the
Brown-Warner (1980, 1985) crude-dependence adjustment:
ts_sd is now the ESTIMATION-window AAR series’s standard
deviation, with df = n_estimation_days - 1 exposed via
attr(result, "caltime_df") and threaded through to
adjust_p_values() and tidy(). This is a
genuine convention change (not a silent-input-only fix) – valid-input
caltime_t/ccaltime_t values differ from the
prior release; see NEWS.md. Locked in
test_reeval_statistical_fixes.R: doubling a pure event-day
shock exactly doubles caltime_t on that day (proving the
denominator no longer depends on the event window).n_pos used
>= 0; fixed to strict > 0.A regression test locks the corrected per-day broadcast values and
asserts they are distinct; the golden values were RE-PINNED for the A7
redefinition (see test_golden_values.R, “re-pinned
2026-09-24: A7”).
Citation: Brown, S. J., & Warner, J. B. (1980). Measuring security price performance. Journal of Financial Economics, 8(3), 205–258. Brown, S. J., & Warner, J. B. (1985). Using daily stock returns: The case of event studies. Journal of Financial Economics, 14(1), 3–31. The calendar-time portfolio approach is also due to Jaffe (1974) and Mandelker (1974), popularized for long-horizon studies by Fama (1998) and Mitchell & Stafford (2000) as a control for cross-sectional dependence.
The Kolari & Pynnonen (2010) test corrects the BMP standardized cross-sectional test for cross-sectional correlation of abnormal returns. When events share calendar time, their abnormal returns are positively correlated, which biases the BMP variance downward and over-rejects the null; the KP adjustment scales the BMP statistic down by a factor derived from the average pairwise correlation.
| Convention | Choice | Note |
|---|---|---|
| Base statistic | BMP cross-sectional t (SAR = AR / model sigma,
bmp_t = sqrt(N) mean(SAR)/sd(SAR)) |
Identical to BMPTest; KP scales it. |
| Usable events | An event contributes to r_bar only if its
estimation-window SAR series has >= 2 finite values, a
finite model sigma, AND a strictly positive (non-constant) SAR standard
deviation |
2026-09-24 item A3: a NON-usable event is excluded (once, with a contract warning/error naming it) – never silently folded into the correlation matrix as a degenerate (constant or all-NA) series. |
| Average correlation | r_bar = average off-diagonal pairwise correlation of
estimation-window SARs, over the USABLE events only |
r_bar = (sum(cor) - n_usable) / (n_usable (n_usable - 1));
events (not firms) are the cross-sectional units, so the SAR matrix is
pivoted by event_id. Numerically identical to the
pre-2026-09-24 formula when every event is usable. |
| Adjustment factor | kp_adj_t = sqrt((1 - r_bar) / (1 + (n_t - 1) r_bar))
PER DAY |
Kolari-Pynnonen (2010) correction; n_t is THAT DAY’s
n_valid_events (2026-09-24 item A3: previously a single
scalar n <- aar_stats$n_valid_events[1] – the FIRST
day’s count – was applied uniformly to every day, silently wrong
whenever the per-day valid-event count varies).
kp_adj < 1 when r_bar > 0, shrinking the
statistic toward zero. |
| Point statistic | kp_t = bmp_t * kp_adj_t |
– |
| Cumulative statistic | ckp_t = cbmp_t * kp_adj_t' |
kp_adj_t' uses the cumulative SAR’s own per-day
n_valid (from cum_sar), not the AAR-level
n_valid_events. |
| Degenerate guard | kp_t/ckp_t are NA (NEVER
kp_adj = 1) when fewer than 2 usable events remain, an
off-diagonal correlation is non-finite, or the adjustment’s
numerator/denominator is invalid |
2026-09-24 item A3: the pre-fix fallback silently substituted
kp_adj = 1 (i.e. the UNADJUSTED BMP value) whenever the
correction was undefined – indistinguishable from “no correlation
correction needed” when it actually meant “the correction could not be
computed”. Exactly one warning/error per compute() call
covers both the usable-event exclusion and this NA substitution (never
two). |
| All-NA event exclusion | An event with ZERO finite event-window ARs is excluded from the whole statistic (2026-09-24 item A2) | See “Multi-event all-NA exclusion” below; distinct from the estimation-window “usable events” check above (A3) – an event can have valid event-window ARs but a non-usable estimation-window SAR series, or vice versa. |
| Distribution | Approximately t_{N-1} |
– |
| p-value sidedness | Two-sided by default | – |
Audit result: verified against Kolari & Pynnonen (2010);
two silent-wrong defects fixed 2026-09-24 (items A2, A3). The
BMP base statistic and the
sqrt((1 - r_bar)/(1 + (n_t - 1) r_bar)) adjustment formula
match Kolari-Pynnonen. The 2026-09-24 re-evaluation found: (1) a
degenerate (all-NA or constant) event’s estimation-window SAR series was
silently folded into the r_bar correlation matrix instead
of being excluded, and when the adjustment was undefined the code fell
back to kp_adj = 1 – silently reporting the UNADJUSTED BMP
value as if it were KP-corrected; and (2) the per-day window-size term
used only the FIRST day’s n_valid_events, applied uniformly
to every day, instead of that day’s own count. Both are fixed; re-pinned
in test_golden_values.R against the package pipeline’s own
output on the three-event fixture (where the events’ estimation SARs are
highly correlated by construction, r_bar ~ 0.983, so
kp_adj ~ 0.076 shrinks the BMP t sharply – unaffected here
since all three events remain usable and n_valid_events is
constant across days in this fixture), and locked in
test_reeval_statistical_fixes.R against a 6-event fixture
with one constant-estimation-window (non-usable) event and a per-day
varying n_valid_events, at absolute tolerance
1e-8.
Citation: Kolari, J. W., & Pynnonen, S. (2010). Event study testing with cross-sectional correlation of abnormal returns. Review of Financial Studies, 23(11), 3996–4025.
PermutationTest is exported but its
compute() body is empty (returns NULL) and it
is not wired into any statistics set or parameter set – it is not on any
pipeline path. It is recorded here for completeness; a decision to
implement or remove it is deferred (see the audit log).
confidence_type (2026-09-24
re-evaluation, item A10)Every TestStatisticBase subclass accepts
confidence_level and confidence_type at
construction, but confidence_type was never read by any
compute() method – every p-value computed anywhere in
EventStudy is two-sided. initialize() now validates
confidence_type with
match.arg(c("two-sided", "less", "greater")) (an invalid
value errors) and emits ONE warning at construction when a non-default
value (“less” or “greater”) is supplied, stating that it is currently
ignored. The value is still stored (for forward compatibility) but has
no effect; one-sided p-values are NOT implemented by this fix. This is a
documented convention (no p-value formula changed) with an added
validation/warning surface.
cross_sectional_regression() regresses per-event CARs on
user-supplied firm characteristics. Prior to 2026-09-24 it silently
tolerated three sample- integrity hazards:
| Hazard | Behavior now |
|---|---|
Duplicated event_id in the data
argument |
Errors (eventstudy_error_bad_argument) naming the
duplicated id(s) – a duplicate would otherwise inflate the join
many-to-many. |
car_window extending outside the event window of any
event |
Errors (eventstudy_error_bad_argument) naming the
window, the available range, and the offending event(s), instead of
silently summing over rows that do not exist for that event. |
A task event with no matching row in data |
One warning listing the count/ids, then dropped (unchanged: an empty match set still errors). |
A missing abnormal return inside car_window for an
otherwise-matched event |
That event’s CAR is NA (.extract_cars() no
longer uses na.rm = TRUE, so ANY missing AR in the window
makes the whole-window CAR NA, not the sum of the finite
subset) and it is excluded from the regression with one warning listing
the event(s). car_by_group(), car_quantiles(),
and plot_car_distribution() inherit the same NA-CAR
semantics from the shared .extract_cars() helper WITHOUT an
additional warning (their own na.rm = TRUE summary
statistics already tolerate NA rows silently, as documented). |
This is a sample-integrity fix (T-sfl-02 in the threat register), not a formula change: the OLS regression itself, robust-SE handling, and CAR extraction for a fully-matched, non-degenerate window are unchanged.
bootstrap_test() computes bootstrap p-values for
AAR/CAAR test statistics. Two silent-wrong defects were found and
fixed:
observed_caar / caar_t were previously built
as cumsum() of the cross-event DAILY mean AR
(na.rm = TRUE), while each bootstrap draw built its CAAR as
the MEAN over events of each event’s own
cumsum(coalesce(AR, 0)). The two constructions coincide
only when every event has a fully-observed event window; with a partial
gap in ONE event (but not all), they diverge, and only the
per-event-CAR-mean construction is the correct STATS-03-consistent
comparison for what the bootstrap draws compute. Fixed so
observed_caar and caar_t use the SAME
per-event coalesce-to-0 CAR construction as the draws; on
NA-free input this is numerically identical to the old
cumsum(aar) form.NA
(e.g. a Rademacher sign-flip combination that zeroes the cross-sectional
SD) was previously counted as “did not exceed the observed statistic”
while still inflating the n_boot + 1 denominator – silently
biasing the p-value toward significance (too small) whenever NA draws
are common. Fixed to track the number of VALID (finite) draws per day
and use p = (exceed + 1) / (n_valid_draws + 1), with
NA when the observed statistic is NA or no
draw produced a finite statistic.firm_symbol (one weight per unique firm per draw, shared
across that firm’s events) – a deliberate prior fix; firm-level clusters
are the coarser, correlation-robust choice when a firm recurs across
multiple events. The 2026-09-24 re-evaluation confirmed this is
intentional and made NO change; it is locked with a hand-replication
test (test_bootstrap.R).| Component | Result | Citation |
|---|---|---|
| Market Model + AR/CAR t | FIXED 2026-09-24 (A6: insufficient-obs guard now
n_params-based; FEC restricted to complete pairs) |
MacKinlay (1997) |
| Market Adjusted Model | FIXED 2026-09-24 (A8: FEC == sigma exactly, correction factor 1) | MacKinlay (1997) |
| Comparison Period Mean Adjusted Model | verified, no change | Brown-Warner (1985); MacKinlay (1997) |
| Fama-French Three-Factor Model | FIXED 2026-09-24 (A6: insufficient-obs guard strengthened) | Fama-French (1993) |
| Fama-French Five-Factor Model | FIXED 2026-09-24 (A6: insufficient-obs guard strengthened) | Fama-French (2015) |
| Carhart Four-Factor Model | FIXED 2026-09-24 (A6: insufficient-obs guard strengthened) | Carhart (1997) |
| Custom Model | documented convention (user-supplied prediction; reduces to Market
Model when loss_market_cap = 0) |
MacKinlay (1997) |
| BHAR Model + BHARTTest | verified, no change | Barber-Lyon (1997); Lyon-Barber-Tsai (1999) |
| Volume Model | verified, no change | Campbell-Wasley (1996); Brown-Warner (1985) |
| Volatility Model | verified, no change | Beaver (1968); Patell-Wolfson (1979) |
| Rolling-Window Model | FIXED 2026-09-24 (A1: complete-pair restriction inside each rolling window) | Fama-MacBeth (1973); MacKinlay (1997) |
| GARCH Model | verified, no change (identity-pinned, skip-guarded on rugarch) | Engle (1982); Bollerslev (1986) |
| DCC-GARCH Model | verified, no change (identity-pinned, skip-guarded on rmgarch) | Bollerslev (1990); Engle (2002) |
| AR / CAR t-tests (ARTTest, CARTTest) | verified, no change (documented:
forecast_error_corrected_sigma_car intentionally unused,
A11) |
MacKinlay (1997) |
| BHAR t-test (BHARTTest) | verified, no change | Barber-Lyon (1997); Lyon-Barber-Tsai (1999) |
| Multi-event all-NA exclusion (shared mechanism) | FIXED 2026-09-24 (A2: all-NA events excluded from every CAR-based multi-event statistic, once per group) | – |
| Cross-Sectional t-test (CSectTTest) | FIXED 2026-09-24 (A2 exclusion; A9: strict
n_pos > 0) |
Brown-Warner (1985) |
| Patell Z-test (PatellZTest) | FIXED 2026-09-24 (A2 exclusion; A6:
m/k redefinition; A9: strict
n_pos > 0) |
Patell (1976) |
| BMP test (BMPTest) | FIXED 2026-09-24 (A2 exclusion; A11: roxygen corrected, docs-only) | Boehmer-Musumeci-Poulsen (1991) |
| Sign test (SignTest) | FIXED 2026-09-24 (A2 exclusion) | Brown-Warner (1985); Cowan (1992) |
| Generalized Sign test (GeneralizedSignTest) | FIXED 2026-09-24 (A2 exclusion; A4:
p_hat grouped by event_id) |
Cowan (1992) |
| Rank test (RankTest) | FIXED (original audit: rank_z
broadcast); FIXED 2026-09-24 (A4: grouped by
event_id) |
Corrado (1989) |
| Calendar-Time Portfolio test (CalendarTimePortfolioTest) | FIXED (original audit:
caltime_t/ccaltime_t broadcast); FIXED
2026-09-24 (A7: Brown-Warner estimation-window denominator,
redefinition; A9: strict n_pos > 0) |
Brown-Warner (1980, 1985); Jaffe (1974); Fama (1998) |
| Kolari-Pynnonen test (KolariPynnonenTest) | FIXED 2026-09-24 (A2 exclusion; A3: usable-events
r_bar, per-day adjustment, never kp_adj = 1
fallback) |
Kolari-Pynnonen (2010) |
| PermutationTest | unwired stub – empty compute(), not on pipeline path;
deferred |
– |
bootstrap_test() |
FIXED 2026-09-24 (A12a: observed/draw CAAR construction aligned; A12b: NA draws excluded from denominator; A12c: clustering unit confirmed, no change) | – |
cross_sectional_regression() |
FIXED 2026-09-24 (A5: duplicate-id error,
out-of-range car_window error, unmatched/NA-CAR
warnings+exclusion) |
– |
TestStatisticBase$confidence_type |
FIXED 2026-09-24 (A10: validated, warns once when non-default; no formula change) | – |
The original audit covered all 13+ return models and all wired test
statistics and found two broadcast-bug implementation errors (RankTest,
CalendarTimePortfolioTest). The 2026-09-24 re-evaluation covered the
same surface plus bootstrap_test() and
cross_sectional_regression(), and found twelve additional
silent-wrong-statistic defects (items A1-A9, A11-A12; A10 is a
validation/warning addition, no formula change; A11 is docs-only). Every
defect is fixed, locked by a regression test that fails on the pre-fix
code (test_reeval_statistical_fixes.R), and golden values
affected by a genuine redefinition (A7, A8, and the Patell/KP/Rank/GSign
pins touched by A2/A3/A4/A6/A9) are re-pinned in
test_golden_values.R with an explanatory comment. No
valid-input behavior changed for any component NOT listed above as
fixed.
2026-09-24 re-evaluation fixes (12 items, A1-A9 + A11-A12; A10 is a validation addition):
x_bar/ss_xx from
ALL index values in a window, including rows where the firm return was
NA, biasing beta when NA-firm rows co-occur with index outliers. Fixed
to restrict each window to complete pairs.r_bar, and an
undefined adjustment silently fell back to kp_adj = 1 (the
unadjusted BMP value). Fixed to exclude non-usable events from
r_bar and return NA (never a silent 1) when the adjustment
cannot be computed; also fixed the per-day window-size term (previously
the first day’s count applied to every day).p_hat estimation grouped by
firm_symbol, silently pooling a recurring firm’s data
across its distinct events. Fixed to group by
event_id.event_ids, out-of-range car_windows,
and NA-CAR events were silently tolerated. Fixed with explicit
errors/warnings and exclusion.n_params + 1; Patell’s k was derived as
length(resid) - df instead of read from the model; Patell’s
m used the raw row count instead of the finite count;
MarketModel’s FEC included incomplete-pair rows. All fixed.ts_sd was computed from the event-window portfolio series
itself (self-referential with the numerator). Redefined to the
Brown-Warner (1980, 1985) estimation-window AAR standard deviation.sigma * sqrt(1 + 1/m)) for a
model that estimates NOTHING. Fixed to FEC == sigma exactly.n_pos used
>= 0 in CSectT, Patell, and CalendarTime (inconsistent
with SignTest’s > 0). Fixed to strict
> 0 everywhere.observed_caar used a different
construction than the draws; NA draws were counted as “did not exceed”
while still inflating the denominator. Both fixed.A10 (TestStatisticBase confidence_type validation) adds
a construction-time check and a one-time warning; it changes no computed
statistic.