In this guide, you will run a miniature event study: measure how a stock reacts to its own earnings announcements, after stripping out the market’s general drift. The workflow is the standard one from the event-study literature, simplified to the market-adjusted model:
(Note: A full event study would estimate a stock-specific beta over a pre-event estimation window and test CARs for statistical significance. The market-adjusted shortcut shown here assumes beta ≈ 1—fine for exploration and visualization, but treat any inference with care.)
We study three recent Apple earnings announcements. For each, we will download two calendar weeks on either side—enough slack that the ±2/+5 trading-day window always fits inside the fetched range:
events <- tibble::tribble(
~label, ~event_date,
"FY25 Q4", as.Date("2025-10-30"),
"FY26 Q1", as.Date("2026-01-29"),
"FY26 Q2", as.Date("2026-04-30")
)(Note: Announcement dates are public knowledge—check Apple’s investor relations page for your period of interest and adjust freely.)
Fetching both symbols in one call keeps the benchmark perfectly aligned with the stock’s calendar:
aapl_event <- yf_download_prices(
tickers = c("AAPL", "^GSPC"),
start = as.Date("2026-01-15"),
end = as.Date("2026-02-12"),
interval = "1d"
)
head(aapl_event, n = 4)
#> # A tibble: 4 x 8
#> symbol date open high low close adj_close volume
#> <chr> <dttm> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 ^GSPC 2026-01-15 14:30:00 6890. 6920. 6880. 6905. 6905. 4.90e9
#> 2 AAPL 2026-01-15 14:30:00 231. 232. 230. 231. 230. 4.51e7
#> 3 ^GSPC 2026-01-16 14:30:00 6905. 6935. 6899. 6928. 6928. 5.12e9
#> 4 AAPL 2026-01-16 14:30:00 231. 233. 230. 232. 231. 4.13e7The helper below pivots each window to one row per day with
side-by-side price columns, computes daily returns for both series,
aligns every row to the announcement day (rel_day 0), and
keeps the event window:
compute_ar <- function(label, event_date) {
raw <- yf_download_prices(
tickers = c("AAPL", "^GSPC"),
start = event_date - 14,
end = event_date + 14,
interval = "1d"
)
raw |>
mutate(symbol = if_else(symbol == "^GSPC", "index", symbol)) |>
select(date, symbol, close) |>
pivot_wider(names_from = symbol, values_from = close) |>
arrange(date) |>
mutate(
ret_stock = AAPL / lag(AAPL) - 1,
ret_mkt = index / lag(index) - 1,
ar = ret_stock - ret_mkt
) |>
# Locate the first session on/after the announcement (t = 0)
mutate(rel_day = row_number() - which(as.Date(date) >= event_date)[1]) |>
filter(rel_day %in% -2:5) |>
transmute(label, rel_day, ar)
}Running it for a single event shows the shape of the result—the
announcement reaction lands at rel_day 0 or 1 depending on
whether the release came before or after the bell:
ar_q1 <- compute_ar("FY26 Q1", as.Date("2026-01-29"))
ar_q1
#> # A tibble: 8 x 3
#> label rel_day ar
#> <chr> <int> <dbl>
#> 1 FY26 Q1 -2 -0.0042
#> 2 FY26 Q1 -1 0.0019
#> 3 FY26 Q1 0 0.0031
#> 4 FY26 Q1 1 0.0384
#> 5 FY26 Q1 2 0.0057
#> # i 3 more rowsA diverging bar chart makes the announcement-day pop obvious:
ggplot(ar_q1, aes(x = rel_day, y = ar, fill = ar >= 0)) +
geom_col(width = 0.7, show.legend = FALSE) +
scale_fill_manual(values = c("#c62828", "#2e7d32")) +
scale_x_continuous(breaks = -2:5) +
scale_y_continuous(labels = percent_format(accuracy = 1)) +
labs(
title = "AAPL Abnormal Returns — FY26 Q1 Earnings",
subtitle = "Market-adjusted vs. S&P 500; day 0 = announcement",
x = "Trading days relative to announcement",
y = "Abnormal return",
caption = "Source: Yahoo Finance via yahoofinancer"
) +
theme_minimal(base_size = 12)Now run the helper over every event with map2() and
list_rbind(), then accumulate: thin lines show each
announcement’s CAR path, the thick line their average:
car_paths <- map2(events$label, events$event_date, compute_ar) |>
list_rbind() |>
group_by(label) |>
arrange(rel_day, .by_group = TRUE) |>
mutate(car = cumsum(replace_na(ar, 0))) |>
ungroup()
avg_car <- car_paths |>
group_by(rel_day) |>
summarise(car = mean(car), .groups = "drop")(Note: arrange(rel_day, .by_group = TRUE) sorts
within each event so the running sum never crosses event boundaries;
replace_na() guards the leading NA
return.)
ggplot() +
geom_line(
data = car_paths,
aes(x = rel_day, y = car, group = label),
color = "#9aa5ad", linewidth = 0.5
) +
geom_line(
data = avg_car, aes(x = rel_day, y = car),
color = "#1f77b4", linewidth = 1.2
) +
geom_vline(xintercept = 0, linetype = "dashed", color = "#555555") +
scale_x_continuous(breaks = -2:5) +
scale_y_continuous(labels = percent_format(accuracy = 1)) +
labs(
title = "Average Cumulative Abnormal Return — AAPL Earnings",
subtitle = "Grey: individual announcements; blue: average",
x = "Trading days relative to announcement",
y = "Cumulative abnormal return",
caption = "Source: Yahoo Finance via yahoofinancer"
) +
theme_minimal(base_size = 12)vignette("cookbook", package = "yahoofinancer")) is a
natural next step toward proper factor controls.Below is the complete, self-contained workflow in a single copy-pasteable script:
library(yahoofinancer)
library(dplyr)
library(tidyr)
library(purrr)
library(ggplot2)
library(scales)
# 1. Events: three earnings announcements, two weeks of slack each side
events <- tibble::tribble(
~label, ~event_date,
"FY25 Q4", as.Date("2025-10-30"),
"FY26 Q1", as.Date("2026-01-29"),
"FY26 Q2", as.Date("2026-04-30")
)
# 2. Market-adjusted abnormal returns over [-2, +5]
compute_ar <- function(label, event_date) {
yf_download_prices(c("AAPL", "^GSPC"),
start = event_date - 14,
end = event_date + 14
) |>
mutate(symbol = if_else(symbol == "^GSPC", "index", symbol)) |>
select(date, symbol, close) |>
pivot_wider(names_from = symbol, values_from = close) |>
arrange(date) |>
mutate(
ret_stock = AAPL / lag(AAPL) - 1,
ret_mkt = index / lag(index) - 1,
ar = ret_stock - ret_mkt
) |>
mutate(rel_day = row_number() - which(as.Date(date) >= event_date)[1]) |>
filter(rel_day %in% -2:5) |>
transmute(label, rel_day, ar)
}
# 3. Cumulate per event and average across events
avg_car <- map2(events$label, events$event_date, compute_ar) |>
list_rbind() |>
group_by(label) |>
arrange(rel_day, .by_group = TRUE) |>
mutate(car = cumsum(replace_na(ar, 0))) |>
ungroup() |>
group_by(rel_day) |>
summarise(car = mean(car), .groups = "drop")
# 4. Plot the average CAR path
ggplot(avg_car, aes(x = rel_day, y = car)) +
geom_line(color = "#1f77b4", linewidth = 1.2) +
geom_vline(xintercept = 0, linetype = "dashed", color = "#555555") +
scale_x_continuous(breaks = -2:5) +
scale_y_continuous(labels = percent_format(accuracy = 1)) +
labs(
title = "Average CAR — AAPL Earnings Announcements",
x = "Trading days relative to announcement",
y = "Cumulative abnormal return"
) +
theme_minimal(base_size = 12)In this guide, you learned how to:
yf_download_prices(start, end).rel_day and slice the [-2, +5]
window..by_group = TRUE) and average them into a
single readable chart.facet_wrap(~ symbol).interval = "60m" (max 730-day lookback applies) to
separate the opening-gap reaction from same-day drift.vignette("cookbook", package = "yahoofinancer").