The 2026-09-24 package re-evaluation found twelve
silent-wrong-statistic defects across return models, multi-event test
statistics, bootstrap inference, and cross-sectional regression. Every
fix ships with a failing-first regression test in
tests/testthat/test_reeval_statistical_fixes.R and, where
the fix redefines a valid-input result, a re-pinned golden value in
tests/testthat/test_golden_values.R. See
vignettes/statistical-conventions.Rmd for the full audit
log and citations.
x_bar/ss_xx/ residuals/sigma. Previously, rows
where the firm return was NA but the index was not still contributed to
x_bar/ss_xx, biasing beta whenever NA-firm
rows co-occur with index outliers. fit() and
calculate_statistics() now share the same complete-pair
count for df/FEC.CSectTTest, PatellZTest,
SignTest, GeneralizedSignTest,
BMPTest, KolariPynnonenTest), applied once per
group (at most one warning per group, not one per configured statistic).
Previously it was coalesced to 0 and silently understated cross-event
dispersion. The pre-existing STATS-03 convention (a partial gap
contributes 0 to that event’s own CAR) is unchanged.r_bar
cross-sectional correlation estimate (reported once); if fewer than 2
usable events remain, kp_t/ckp_t are
NA instead of silently reporting the unadjusted BMP value.
The per-day KP adjustment now uses THAT DAY’s
n_valid_events instead of the first day’s count applied
uniformly to every day.event_id, not firm_symbol, for
ranking/centering and p_hat estimation. Previously a firm
recurring across multiple events had its data silently pooled across
those events.cross_sectional_regression()
sample-integrity hardening. Duplicated event_ids
in the data argument now error; a car_window
extending outside any event’s available event window now errors; task
events with no matching data row are dropped with one
warning; an event whose CAR is NA because an abnormal
return inside car_window is missing is excluded with one
warning (.extract_cars() no longer sums with
na.rm = TRUE, so ANY missing AR in the window makes the
whole CAR NA;
car_by_group()/car_quantiles()/plot_car_distribution()
inherit this NA-CAR semantics without an additional warning).n_params-based, not a flat threshold, for
MarketModel and every LinearFactorModel
subclass (FF3, FF5, Carhart4); a model that fits with fewer than 30
valid estimation observations (the same recommended minimum as
validate_task()’s default) now emits one advisory warning
in both modes. PatellZTest’s k is now read
explicitly from each model’s statistics$n_params
(MarketModel 2, FF3 4, Carhart4 5, FF5 6, CPMA 1, MarketAdjusted 0)
instead of being derived as length(resid) - df (which was
silently wrong whenever df reflected an unrelated
correction); m now counts finite estimation-window abnormal
returns instead of the raw row count. MarketModel’s
forecast-error correction now uses complete pairs only for
mean(Rm_est)/SS_market, matching exactly the
rows lm() used.CalendarTimePortfolioTest denominator
redefined to the Brown-Warner (1980, 1985) estimation-window AAR
standard deviation. Previously ts_sd was computed
from the event-window portfolio series itself (self-referential with the
numerator, so a larger event-day shock inflated both).
caltime_t/ccaltime_t now use degrees of
freedom n_estimation_days - 1, exposed as
attr(result, "caltime_df") and read by both
adjust_p_values() and tidy(). This is
a valid-input redefinition:
caltime_t/ccaltime_t values differ from the
initial 0.66.0 submission; golden values are re-pinned.MarketAdjustedModel’s forecast-error
correction is now forecast_error_corrected_sigma == sigma
exactly (correction factor 1), because the model estimates
nothing from the estimation window. Previously it wrongly reused the
constant-MEAN correction formula (sigma * sqrt(1 + 1/m)),
which is only valid for a model that DOES estimate one parameter (a
mean), like ComparisonPeriodMeanAdjustedModel (unchanged).
This is a valid-input redefinition; golden values are
re-pinned.n_pos = sum(AR > 0) /
n_neg = sum(AR <= 0) in
CSectTTest, PatellZTest, and
CalendarTimePortfolioTest (previously
>= 0/< 0), matching the pre-existing
SignTest convention. An abnormal return of exactly 0 now
counts as non-positive everywhere.TestStatisticBase$confidence_type is now
validated with
match.arg(c("two-sided", "less", "greater")) (an invalid
value errors) and emits one warning at construction when a non-default
value is supplied, stating that every p-value in EventStudy is currently
two-sided and the value has no effect. No p-value formula changed;
one-sided p-values are not implemented.BMPTest roxygen documentation corrected
(docs-only, no formula change). The docstring incorrectly
claimed standardization by the forecast-error-corrected sigma; both
BMPTest and KolariPynnonenTest standardize by
the MODEL sigma (model$statistics$sigma), which is what the
code always computed.bootstrap_test() observed/draw
construction aligned; NA draws excluded from the p-value
denominator. observed_caar now uses the SAME
per-event coalesce-to-0 CAR construction as the bootstrap
draws (matches cumsum(observed_aar) exactly on NA-free
input, but differs from it under a partial-window gap, where the
per-event construction is correct). A bootstrap draw whose statistic is
NA (e.g. a sign-flip combination that zeroes the
cross-sectional SD) is now excluded from BOTH the exceedance count and
the denominator, instead of being counted as “did not exceed” while
still inflating n_boot + 1. The bootstrap weight clustering
unit (firm_symbol) is confirmed correct and unchanged.ModelBase, TestStatisticBase,
and ReturnCalculation are now exported with
roxygen documenting the subclass contract expected by
run_event_study()
(fit()/abnormal_returns(),
compute(), calculate_return(), and the
statistics fields consumed downstream, including the
optional n_params field added by A6). Previously these base
classes existed but were unexported, so vignettes and skills advertising
“subclass ModelBase” referenced a symbol users could not
actually reach with EventStudy::ModelBase.generics::tidy(task) now dispatches to
tidy.EventStudyTask(). generics added
to Suggests; tidy.EventStudyTask() remains directly
callable and exported as before.modern-did-estimators.Rmd and
automated-reports.Rmd previously called
PanelEventStudyTask$new() with non-existent arguments
(data/unit_col/time_col/treatment_col/outcome_col);
fixed to the real
(panel_data, unit_id, time_id, outcome, treatment, treatment_time).
cross_sectional_regression(characteristics = ...) fixed to
data = .... automated-reports.Rmd’s “Available
sections” table and every sections = c(...) example
previously listed section keys (summary, data,
single_event, multi_event,
cross_sectional, panel) that do not exist in
the report template; corrected to the real keys
(exec_summary, data_methods,
results, diagnostics, robustness,
references, appendix).statistical-conventions added as an article;
dieselgate/earnings_surprises now listed
explicitly in “Data & Datasets”;
degenerate-input-contract,
eventstudy-shape-contracts, and
eventstudy-deprecation grouped under a new “Contracts &
Lifecycle” reference section; report_table added to “Export
& Reporting” and its \keyword{internal} tag removed (it
is exported and advertised).DT and patchwork
removed from Suggests. download_stock_data()’s
quantmod fallback branch now guards zoo::index() with its
own requireNamespace("zoo") check before any download..claude/skills/es-capabilities/SKILL.md
signatures corrected for run_event_study() and
validate_task() to match formals(); a scripted
scan of every vignette/README/skill code chunk confirms zero remaining
argument-name drift against the installed package..claude/skills/es-advisor/reference/interpreting-diagnostics.md
rewritten to the real es_diagnostics() structure
(meta, estimation_window,
event_window, cross_sectional,
contract_state, aggregate_summary), verified
field-by-field against a live run, with the 8 offline knowledge-base
rule thresholds restated in terms of the real field paths they key off.
The previous version documented a fabricated structure
(n_events, per_event,
mean_r_squared, etc.) that did not exist on the returned
object.Test-suite quality hardening so vacuous or always-skip tests no
longer mask regressions, and so the full NOT_CRAN=true
suite runs with WARN 0 (every warning a test triggers is
now explicitly expected by class/regexp, or muffled at its source when
the fixture is intentionally short).
adjust_p_values() gains
explicit-formula tests for its KolariPynnonenTest, GeneralizedSignTest,
RankTest and CalendarTimePortfolioTest branches, comparing against
2*pt(-abs(t), df)/2*pnorm(-abs(z))
directly.firm_symbol) and shows it
differs from an event-level clustering alternative on the same seed; the
two Patell Q_i tests now assert the exact
aar_z from an independent hand computation
(k=2 and k=4); the
LinearFactorModel FEC test now asserts the exact hat-matrix
value sigma*sqrt(1+h_t); the HAC-SE test now asserts
equality with sandwich::NeweyWest() on the same
lm() fit.lm() two-way-fixed-effects fit on the same
deterministic seeded fixture (static_twfe estimate +
cluster-robust SE; dynamic_twfe every non-base event-time
coefficient), replacing the previous loose numeric bounds..rank_events_for_cap() real test:
an event genuinely unfitted via a degenerate estimation window (with its
one contract warning asserted explicitly) is shown to always rank first
(anomaly score Inf) and always survive a
max_events cap, replacing the previous unconditional
skip().tryCatch()-to-TRUE handler; it now asserts
is_fitted unconditionally and skips ONLY on a
narrowly-matched non-convergence warning. The MarketModel
FEC “effective obs count” test no longer hides its assertions behind
if (is_fitted)..try_download() narrowed to
convert only network/HTTP-style failures into skip() (by
condition class when available, otherwise a narrow message pattern); any
other error (a parse/logic bug) now fails the test. Covered by two new
direct tests of the helper itself.test_that() block that draws random
numbers without its own seed now has one, across
test_edge_cases.R, test_models.R,
test_multi_event_statistics.R,
test_cross_sectional.R,
test_synthetic_control.R and test_intraday.R;
test_intraday.R’s Sys.time()-based fixtures
were replaced with fixed UTC POSIXct literals.test_execute.R (each exercised model’s
AR/alpha/beta checked against a
direct lm() or arithmetic computation) and
test_export.R (exported CSV
AR/CAR/AAR values checked to
equal the task’s own values, round-tripped through the file).CalendarTimePortfolioTest case moved from
test_bhar_test_statistics.R to
test_multi_event_statistics.R (it is not a BHAR test); the
browser()-source-grep check in
test_aar_test_statistics.R replaced with a behavioural
PatellZTest$compute() test against a hand computation; the
single weak test in test_caar_test_statistics.R folded into
a real numeric CSectTTest check.expect_warning(..., regexp = / nested for multiple
independent warnings) or muffled by class via the new
muffle_short_window() helper
(tests/testthat/helper-warnings.R) for fixtures whose short
estimation window is intentional (golden/invariant/ numerical-stability
fixtures, never a blanket suppressWarnings()). A new
test_execute.R test locks that fit_model()
collapses N short-window events into exactly ONE
eventstudy_short_estimation_window warning listing the
event ids.This release completes a signature-consistency audit of the full
public API and wires a formal deprecation lifecycle (base
.Deprecated() + optional lifecycle shim). All
deprecated argument names continue to work and emit exactly one
deprecation warning pointing to the replacement.
plot_stocks(): do_sample is deprecated in
favour of sample_symbols (#APIS-01). The new name is
noun-first, consistent with the sibling parameter
max_symbols. Pass the old name and you receive one
deprecation warning; the result is identical. (#APIS-04).deprecate_arg() in
R/deprecation.R. Always calls base
.Deprecated() unconditionally; additionally calls
lifecycle::deprecate_warn() when lifecycle is
installed (Suggests-only, never a hard dependency). (#APIS-04)R/shape_contracts.R provides an opt-in return-shape
contract for the pipeline’s result tibbles. Enable with
options(EventStudy.shape_contracts = TRUE). When enabled,
the pipeline checks that the column names and types of the single-event
statistics tibbles (ART, CART) and the
multi-event AAR/CAAR tibble (CSectT) match the canonical
expected shapes. On any mismatch exactly one warning() is
emitted naming the context and the specific drift; the contract never
calls stop(). Correctly-shaped
is_fitted = FALSE degenerate outputs (same column names,
NA-propagated values) are treated as valid shapes and
produce no warning. The option defaults to FALSE so
valid-input behaviour and the existing test suite are completely
unaffected. (#APIS-02)rcmdcheck against
the installed package (not load_all) across
multiple operating systems, catching install-vs-load_all
divergence in NAMESPACE/imports before it reaches CRAN._R_CHECK_FORCE_SUGGESTS_
forced on, exercising every optional-Suggests code path so the
requireNamespace() guards are verified end-to-end.report_table() is now an exported function; the public
API snapshot accepts it.tempdir().es_report,
generate_report, run_event_study,
recommend_stat, flag_robustness,
nonparametric_intraday_test) were converted from
\dontrun{} to \donttest{} and made
self-contained (bundled dieselgate data,
tempdir()-only output). Only the genuine network +
credentials provider examples remain \dontrun{}.R CMD check --as-cran on the built source tarball is
clean (0 errors, 0 warnings; the only NOTE is the expected
archived-package incoming-feasibility acknowledgement), and win-builder
R-devel / R-release checks were dispatched.The “Polish” milestone lifts the felt quality of the package — brand identity, plot and report aesthetics, API and message ergonomics, and documentation — with no change to statistical results. Behavior on valid inputs is unchanged; this release is purely additive.
man/figures/logo.png and hex sticker at
man/figures/logo-hex.png (#BRAND-01).template.bslib and
pkgdown/extra.scss, with a restyled home card gallery and
numeric-badge strip (#BRAND-05, #BRAND-06).experimental to
stable (#BRAND-07).theme_eventstudy() ggplot2 theme
and es_colours palette (exported from
R/theme.R). The palette is the colorblind-safe Okabe-Ito
set aligned to the ecosystem brand (#VIZ-01, #VIZ-02).plot_event_study(), plot_diagnostics(), and
the single/multi-event helpers) now draw from es_colours
instead of hardcoded colours, for a consistent, accessible look across
the package (#VIZ-03).es_report() tables now render via
tinytable when available, with a
byte-compatible knitr::kable() fallback when it is not —
styled consistently across the HTML, PDF, Word, and Markdown formats
(#VIZ-04, #CRAN-05).ragg) instead of a single global
width (#VIZ-05, #VIZ-06).report.css,
injected only on the HTML output path; PDF/Word/Markdown output is
unaffected (#VIZ-07).print.* methods now return invisible(x)
for clean piping, and format.* methods were added where
missing (#API-01, #API-02).stop()/warning() calls are now
classed eventstudy_* conditions (via
rlang::abort()/rlang::warn()) whose messages
name the offending argument and value, so failures are catchable by
class and easier to diagnose (#API-03, #API-04).verbose= quiet mode was added; the default is
byte-identical to prior behavior (#API-05). Deprecation audit completed
with no new dependency (#API-06).@family and @seealso cross-links
across the exported API, grouping functions into cohesive clusters for
easier navigation (#DOCS-01).pkgdown::check_pkgdown() wired into CI so
reference-index drift is caught automatically (#DOCS-03).tinytable,
patchwork, ragg) were added, all guarded by
requireNamespace(); no new hard dependencies and no new
R CMD check findings versus the prior baseline
(#CRAN-01).New es_report() orchestrator. A
single function call converts a fitted EventStudyTask into
a complete, publication-ready report. The function deep-clones the task
(non-mutation guarantee), runs es_diagnostics() to harvest
diagnostic metadata, assembles a grounded narrative via
generate_report() (assembled once before any format loop),
and renders output to one or more formats. Returns a named character
vector of output path(s) visibly, so the path prints at the REPL without
an explicit print().
Multi-format output. es_report()
accepts format = c("html", "pdf", "word", "md") and renders
one file per format sharing a common basename. All formats are fully
offline by default (no API key, no network required); an optional
provider argument enables LLM-grounded narrative when
available.
Additive
run_event_study(report = FALSE, report_args = list())
convenience. The pipeline entry-point gains two new optional
arguments. When report = FALSE (the default), behaviour is
byte-identical to the previous release – no allocation overhead, no side
effects. When report = TRUE, es_report() is
called after the pipeline completes and the rendered path(s) are
attached as attr(task, "report_path").
DESCRIPTION URL and README
now link to the live docs site..Rbuildignore excludes _pkgdown.yml,
docs/, and pkgdown/ so the site scaffolding is
kept out of the CRAN source tarball; no source or behaviour change.download_stock_data() now fails clearly when
the source returns no data. When
tidyquant::tq_get() cannot reach the source (network
outage, rate limit) or the ticker is invalid, it returns a bare logical
instead of a data frame. Previously this surfaced as a cryptic
no applicable method for 'transmute' error; the function
now detects the empty result and stops with an actionable message naming
the symbols and source. No behavior change on valid downloads.
Network download tests skip gracefully on source
outages. The Yahoo Finance and Ken French library tests are
wrapped so a transient source outage skips the test instead of failing
CI (skip_if_offline() only checks generic connectivity, not
the specific source). Fixes intermittent red CI on the R-devel job when
Yahoo was unreachable.
Fixed the single-event AR/CAR confidence bands in
plot_event_study(). Both bands were centered on
zero (+/- z*sigma for AR, +/- z*sqrt(k)*sigma
for CAR) instead of on the estimate, so an abnormal return or CAR that
drifts away from zero (e.g. VW’s -17% AR / -35% CAR in the advisor
vignette) ran far outside its own “confidence band”. The ribbons are now
AR +/- z*sigma and CAR +/- z*sqrt(k)*sigma,
correctly enveloping the plotted path.
Extended dieselgate dataset — the
bundled dataset now covers four German automakers
(VOW.DE, PAH3.DE, BMW.DE, MBG.DE) plus the DAX benchmark across the 2015
Dieselgate emissions scandal, with a two-group request structure: “VW
Group” (directly implicated firms) vs “Other” (peer automakers). This
enables multi-group event studies and CAAR comparisons out of the box.
The four-row request tibble carries the group column;
event_id = 1 remains VOW.DE for backward compatibility. See
?dieselgate.
CI bands on CAR/CAAR plots —
plot_event_study(type = "car", event_id = 1L, confidence_level = 0.95)
draws a 95% confidence ribbon around the cumulative abnormal return
path; the group CAAR comparison plot in the advisor vignette overlays
per-group CI ribbons, making the statistical certainty of the VW Group
crash visually immediate.
Multi-group CAAR comparison — the advisor vignette now shows a side-by-side ggplot2 CAAR path for “VW Group” (CAAR at +10 ≈ -39%, t ≈ -12.6) and “Other” (CAAR at +10 ≈ +1%, t ≈ 0.34, not significant), illustrating the idiosyncratic nature of the shock: VW Group craters while peer automakers barely move.
Mock AI-advisor interpretation block — the advisor vignette’s static LLM-output example is updated to reflect the multi-group results, grounded in the actual computed diagnostics (beta ~1.09, R-squared ~0.74, Shapiro-Wilk p ~0.004, VW Group CAAR t-statistic ~-12.6, near-zero Other CAAR). The block is clearly labelled as a captured/illustrative example, not evaluated at build time.
dplyr was not attached. SimpleReturn
/ LogReturn called a bare lag(), but
dplyr::lag was not imported into the package namespace, so
lag resolved to stats::lag, which no-ops on a
plain numeric vector. Every return then computed as 0 (and
row 1 as 0 instead of NA); the resulting
zero-variance series tripped the market-model variance guard, so
all abnormal returns and CARs came back
NA. This only manifested when dplyr was not on
the search path (e.g. a plain library(EventStudy) session
or the advisor vignette build); the test suite and audit ran with
dplyr attached, masking stats::lag, and never
observed it. dplyr::lag is now imported, so returns are
correct for everyone. Behavior is unchanged for callers who already had
dplyr attached. A regression test locks the namespace
resolution so lag can never silently fall back to
stats::lag again.ai-advisor.Rmd) gains AR and
CAR plots — the abnormal return across the
[-10, +10] event window and the cumulative abnormal return
path, both rendered offline, making the dieselgate crash visible.This release makes the AI advisor’s story legible through a real-world worked example, with no change to existing package behavior or APIs.
dieselgate dataset — a small,
frozen dataset bundling daily adjusted prices for Volkswagen AG
(VOW.DE) and the DAX benchmark index (^GDAXI)
around the 2015-09-18 EPA emissions-scandal disclosure. Loadable via
data(dieselgate) with no network access; it drives a
complete event study end-to-end and recovers the roughly -35% cumulative
abnormal return crash. See ?dieselgate for the documented
schema and provenance, and data-raw/dieselgate.R for the
reproducible fetch script.
“The Grounded AI Advisor” vignette
(vignettes/ai-advisor.Rmd) — explains why the
advisor exists (the grounding invariant: it never fabricates a number)
and how it works (the two-layer architecture: a deterministic
offline grounding layer plus an optional LLM interpretation layer
policed by a runtime guard). It walks through the
dieselgate example, running the pipeline and the
deterministic advisor layer (es_diagnostics(),
recommend_stat(), flag_robustness())
live with no API key, and shows the
es_advise() LLM layer as a clearly-labelled captured static
block. The vignette builds fully offline.
This release ships a complete AI-advisor layer for event study analysis, built on a deterministic offline knowledge base and an optional LLM backend. All advisor features respect CRAN’s no-phone-home policy: network calls are opt-in and fully guarded.
es_diagnostics() — offline
pre-flight diagnostic suite that inspects estimation-window length,
event-window span, event clustering, model residual autocorrelation, and
return normality, returning a structured Diagnostics object
consumable by the advice layer.
es_kb — a curated deterministic
knowledge base (R/advisor_kb.R) mapping diagnostic flags to
actionable methodology recommendations, sourced from MacKinlay (1997),
Boehmer et al. (1991), Patell (1976), and related academic literature.
recommend_stat() and flag_robustness() query
the KB without any network call.
provider() — constructs a provider
configuration for the LLM backend of es_advise(). Supports
Anthropic (provider("anthropic")), OpenAI-compatible
endpoints (provider("openai_compatible", base_url = ...)),
and custom call-back providers. Uses httr2 +
jsonlite (both in Suggests) guarded by
requireNamespace() — never loads unless the user explicitly
calls es_advise() with a provider.es_advise() + Runtime Grounding Guard (Phase 7)es_advise() — LLM-backed advice
function that assembles grounding context from
es_diagnostics() output and the KB, sends a grounded prompt
to the configured provider, and returns a structured Advice
object. A runtime grounding guard validates that the LLM response cites
at least one KB rule; ungrounded responses are downgraded to
"speculative" confidence and a warning is emitted — the
package never silently returns LLM hallucinations as authoritative
advice.
generate_report(advice = ) — the
report pipeline now accepts an optional Advice object; when
supplied, a dedicated “AI Advisor” section is rendered in the output
report with evidence chains and confidence labels.
.claude/skills/es-advisor/SKILL.md — a
Claude Code Agent Skill that surfaces es_diagnostics() +
es_advise() to Claude Code users directly from their AI
assistant. The skill file is excluded from the CRAN tarball via
.Rbuildignore.Advisor Pro is a planned retrieval-augmented premium tier backed by a curated academic knowledge base. It is not yet available. To join the waitlist, visit: https://github.com/sipemu/eventstudy#advisor-pro-waitlist
An optional opt-in footer can be enabled for Advice
print methods:
options(eventstudy.advisor_pro_footer = TRUE)The footer is silent by default and opens no network connections. See
?advisor_pro for details.
This release hardens the package against degenerate, boundary, and
adversarial inputs (Phase 1–3). Every fix below is locked by a named
test that will catch a regression. The cross-component degenerate-input
regression net is provided by
tests/testthat/test_contract_matrix.R (TEST-02, see final
entry).
CONTRACT-01/02: Degenerate-input contract defined in
R/contract.R (.handle_degenerate(),
.resolve_degenerate_mode(); ParameterSet
degenerate_handling field; package option
EventStudy.degenerate_handling). — Locked by
tests/testthat/test_contract.R ::
test_that("CONTRACT-02: ParameterSet default resolves to lenient")
and
test_that("CONTRACT-02: ParameterSet strict field resolves to strict").
CONTRACT-03: Strict mode raises descriptive
stop() naming event_id, firm_symbol, and component on
degenerate input (MarketModel). — Locked by
tests/testthat/test_contract.R ::
test_that("CONTRACT-03: strict mode errors on insufficient obs — names event_id, firm_symbol, MarketModel").
CONTRACT-04: Lenient mode sets
is_fitted = FALSE, emits exactly one warning per
(event_id, firm_symbol), propagates NA through
abnormal_returns, and the pipeline emits exactly one warning per
degenerate event. — Locked by
tests/testthat/test_contract.R ::
test_that("CONTRACT-04: lenient mode on insufficient obs sets is_fitted FALSE + exactly 1 warning")
and
test_that("CONTRACT-04 (pipeline): run_event_study emits exactly ONE warning per degenerate (event_id, firm_symbol)").
CONTRACT-05: Valid-input MarketModel statistics are
byte-stable (baseline fixture). — Locked by
tests/testthat/test_contract.R ::
test_that("CONTRACT-05: valid-input MarketModel statistics match committed baseline within 1e-8").
MarketModel migrated onto
.handle_degenerate() (insufficient obs /
zero-variance index guards). — Locked by
tests/testthat/test_contract.R ::
test_that("CONTRACT-03: strict mode errors on insufficient obs — names event_id, firm_symbol, MarketModel").
MarketAdjustedModel, ComparisonPeriodMeanAdjustedModel,
CustomModel, BHARModel, VolumeModel, VolatilityModel migrated
(each has insufficient-obs and/or zero-variance guard). — Locked by
tests/testthat/test_models.R ::
test_that("VolatilityModel: insufficient obs raises contract error in strict mode")
and
test_that("CustomModel: degenerate input — abnormal_returns returns NA without calling predict(NULL)").
LinearFactorModel, FamaFrench3FactorModel,
FamaFrench5FactorModel, Carhart4FactorModel migrated
(insufficient obs / zero-variance guards for excess_return / factor
columns). — Locked by tests/testthat/test_models.R ::
test_that("All four factor models: fit+abnormal_returns emits exactly one warning on degenerate input").
Per-model CONTRACT-05 baseline fixtures
(MarketAdjusted, CompPeriodMean, Custom, BHARModel, VolumeModel,
VolatilityModel, FF3, FF5, Carhart4). — Locked by
tests/testthat/test_models.R ::
test_that("MarketAdjustedModel: valid-input baseline invariance (CONTRACT-05)"),
test_that("FamaFrench3FactorModel: valid-input baseline invariance (CONTRACT-05)"),
etc.
RollingWindowModel migrated (Guard 1:
insufficient obs; Guard 2: all-NA estimation window; Guard 3:
last-window NA params; df reflects finite pair count, not nrow — WR-04).
— Locked by tests/testthat/test_models_time_varying.R ::
test_that("RollingWindowModel: lenient mode — insufficient obs (Guard 1) — one warning, all-NA ARs"),
test_that("WR-04: RollingWindowModel df reflects finite pair count not nrow (NA-heavy window)").
GARCHModel migrated (insufficient obs,
zero-variance guards; pre-call guards added so
rugarch::ugarchfit() never receives fewer rows than GARCH
order). — Locked by
tests/testthat/test_models_time_varying.R ::
test_that("GARCHModel: lenient mode — insufficient obs — one warning, all-NA ARs"),
test_that("GARCHModel: strict mode — insufficient obs — named error").
DCCGARCHModel migrated (insufficient obs,
zero-variance guards). — Locked by
tests/testthat/test_models_time_varying.R ::
test_that("DCCGARCHModel: lenient mode — insufficient obs — one warning, all-NA ARs").
GARCHModel / DCCGARCHModel
calculate_statistics() failure wrapping
(EXTERNAL-04: tryCatch resets
is_fitted = FALSE, emits named warning, returns NA ARs when
rugarch fitting fails). — Locked by
tests/testthat/test_models_time_varying.R ::
test_that("GARCHModel: calculate_statistics failure resets is_fitted=FALSE + named warning + NA ARs"),
test_that("DCCGARCHModel: calculate_statistics failure → exactly ONE warning (fit+abnormal_returns combined)").
STATS-01: ARTTest, CARTTest,
BHARTTest return NA (not Inf/NaN) when
sigma == 0 (divide-by-zero guard). — Locked by
tests/testthat/test_ar_car_test_statistics.R ::
test_that("STATS-01: ARTTest returns NA (not Inf/NaN) when sigma == 0"),
test_that("STATS-01: CARTTest returns NA (not Inf/NaN) in car_t when sigma == 0"),
test_that("STATS-01: BHARTTest returns NA (not Inf/NaN) in bhar_t when sigma == 0").
STATS-02: BMPTest,
KolariPynnonenTest, PatellZTest,
CSectTTest join on event_id (not
firm_symbol) — fixes GH #7 many-to-many inflation.
— Locked by tests/testthat/test_multi_event_statistics.R ::
test_that("BMPTest does not inflate n_events when firms recur (GH #7)"),
test_that("KolariPynnonenTest does not inflate n_events when firms recur (GH #7)"),
test_that("STATS-02: CSectTTest does not inflate n_events when firms recur").
STATS-04: PatellZTest, SignTest
return NA when n_events == 1 (added
n_events <= 1 guards; BMPTest,
CSectTTest already returned NA via
sd(single_value) = NA). — Locked by
tests/testthat/test_multi_event_statistics.R ::
test_that("STATS-04: PatellZTest aar_z is NA (not finite) when n_events == 1"),
test_that("STATS-04: SignTest sign_z is NA (not finite) when n_events == 1").
PIPELINE-01: prepare_event_study() honours
degenerate mode on missing event date — strict errors naming
component/event/firm; lenient warns and degrades to all-zero windows. —
Locked by tests/testthat/test_prepare.R ::
test_that("missing event date strict: stop() names component, event_id, firm_symbol"),
test_that("missing event date lenient: exactly one warning naming event_id and firm_symbol").
PIPELINE-02: export_results() NA-safe CAR
cumsum (reverted false coalesce — NA must propagate through
cumsum, not be silently replaced with 0; CR-01). — Locked by
tests/testthat/test_export.R ::
test_that("export_results: CAR — interior NA propagates through cumsum (correct NA behavior)"),
test_that("export_results: NA abnormal_returns in AR column preserved as NA (not 0)").
PIPELINE-03: cross_sectional_regression()
singular/collinear guard — returns NA
std.error/statistic/p.value with one warning instead of
crashing; sandwich absent degrades to OLS SEs with a
warning. — Locked by tests/testthat/test_cross_sectional.R
::
test_that("singular design: collinear regressor returns NA std.error/statistic/p.value + one warning"),
test_that("sandwich absent: emits warning() naming robust SEs and OLS fallback").
EXTERNAL-01..03: Panel DiD estimators
(callaway_santanna,
dechaisemartin_dhaultfoeuille,
borusyak_jaravel_spiess) return NULL results
with a warning when the required package is absent or the estimator
fails. — Locked by tests/testthat/test_panel.R ::
test_that("callaway_santanna warns and returns task with NULL results when did is absent").
EXTERNAL-04: estimate_synthetic_control()
empty-donor-pool guard — returns NULL with a
warning instead of crashing when quadprog::solve.QP or the
donor pool is empty. — Locked by
tests/testthat/test_synthetic_control.R ::
test_that("estimate_synthetic_control with quadprog produces valid weights").
WR-01: .degenerate_handled = TRUE set in
calculate_statistics() tryCatch path
— ensures GARCH/DCC failure path also honours one-warning guarantee (set
in the catch block, not lost). — Locked by
tests/testthat/test_models_time_varying.R ::
test_that("GARCHModel: calculate_statistics failure → exactly ONE warning (fit+abnormal_returns combined)").
WR-02: External-package wrapper emits warning when
estimator returns NULL results (was silently producing a NULL
task). — Locked by tests/testthat/test_panel.R ::
test_that("callaway_santanna warns and returns task with NULL results when did is absent").
WR-03: ParameterSet stores normalised
match.arg value — field now holds
"strict" or "lenient" (not raw user input). —
Locked by tests/testthat/test_contract.R ::
test_that("CONTRACT-02: ParameterSet strict field resolves to strict").
IN-01: Removed redundant degenerate_mode /
event_id / firm_symbol field re-declarations
in MarketModel — fields are defined once on
ModelBase. — Locked by
tests/testthat/test_contract.R (model construction and
field assignment exercise these fields).
IN-02: Standardised package option key to
EventStudy.degenerate_handling (was inconsistently
named in early drafts). — Locked by
tests/testthat/test_contract.R ::
test_that("CONTRACT-02: package option overrides default when field is NULL").
IN-03: withr declared in
DESCRIPTION Suggests (was missing; tests using
withr::with_options required it). — Verified by
R CMD check and exercised by all
withr::with_options calls in
tests/testthat/test_contract.R.
CR-01: Reverted export CAR coalesce
— see PIPELINE-02 above.
CR-02: Empty-donor-pool solve.QP
guard — see EXTERNAL-04 above.
test_contract_matrix.R —
cross-component degenerate-input regression net. Table-driven
registry of 25 rows (14 models + 10 contract-covered test statistics + 1
pipeline path) iterated in strict and lenient modes. Locked by
tests/testthat/test_contract_matrix.R ::
test_that("contract-matrix registry is exhaustive")
(asserts length == 25L, sub-counts 14/10/1, exclusions
reconcile to 12 stat classes) and all per-component
test_that("contract-matrix [strict] <label> ...") /
test_that("contract-matrix [lenient] <label> ...")
rows.BMPTest, KolariPynnonenTest, and
PatellZTest joined the per-event model (sigma /
forecast-error-corrected sigma) on firm_symbol instead of
event_id. When a firm appeared in more than one event this
produced a many-to-many join that duplicated rows, inflating
n_events and the test statistics (bmp_t,
kp_t, aar_z). The joins and per-event
groupings now key on event_id (GH #7).