Overview
In this guide you will learn how to fetch historical price data for
multiple stock tickers simultaneously and calculate their cumulative
returns for like-for-like comparison. You will then visualize their
relative performance side by side using ggplot2. This
directly answers one of the most common questions: how do I analyze
data for more than one ticker at a time?
1. Fetching Prices for Multiple Tickers
You can download data for multiple symbols at once using the
yf_download_prices() functional helper. Pass a vector of
ticker symbols (e.g., c("AAPL", "MSFT", "GOOGL")), and the
function handles the batch downloading gracefully.
# Define your portfolio of tickers
symbols <- c("AAPL", "MSFT", "GOOGL", "AMZN")
# Download 1 year of daily historical prices (period = "1y" is the default)
portfolio_prices <- yf_download_prices(
tickers = symbols,
interval = "1d"
)
head(portfolio_prices)
#> # A tibble: 6 × 8
#> symbol date open high low close adj_close volume
#> <chr> <dttm> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 AAPL 2023-08-18 13:30:00 232. 233. 230. 231. 230. 41235600
#> 2 AAPL 2023-08-19 13:30:00 231. 233. 229. 231. 230. 38945200
#> 3 AAPL 2023-08-20 13:30:00 230. 230. 226. 226. 225. 45120300
#> 4 AAPL 2023-08-21 13:30:00 226. 227. 224. 225. 224. 39870100
#> 5 AAPL 2023-08-22 13:30:00 226. 229. 225. 228. 227. 42319800
#> 6 AAPL 2023-08-25 13:30:00 226. 229. 226. 227. 226. 37651000Note on Robustness: If one of the symbols provided is invalid or encounters a network error,
yf_download_prices()will skip it gracefully and still return the data for the remaining valid symbols.
2. Normalizing Prices for Comparison
When comparing stocks with vastly different share prices (e.g., $150 vs. $3,000), plotting raw prices on the same chart isn’t very helpful. Instead, we can normalize the prices. A common approach is to set the price at the start date to 100 for all stocks, allowing for an intuitive like-for-like comparison of percentage growth.
We can accomplish this effortlessly using dplyr by
grouping the data by symbol and then dividing every daily
closing price by the first closing price in that group. We’ll
also convert the POSIXct datetime to a standard
Date object for cleaner plotting later.
portfolio_performance <- portfolio_prices |>
# Group calculations by ticker and ensure chronological sorting within group
group_by(symbol) |>
arrange(date, .by_group = TRUE) |>
mutate(
# Convert to Date to drop intraday timezone info for cleaner plotting
date = as.Date(date),
# Normalize price to 100 on the first day
normalized_price = (close / close[1]) * 100
) |>
ungroup()
# View the latest normalized performance
portfolio_performance |>
group_by(symbol) |>
slice_tail(n = 1) |>
select(symbol, date, close, normalized_price)
#> # A tibble: 4 × 4
#> symbol date close normalized_price
#> <chr> <date> <dbl> <dbl>
#> 1 AAPL 2024-08-21 225. 118.
#> 2 AMZN 2024-08-21 182. 135.
#> 3 GOOGL 2024-08-21 163. 122.
#> 4 MSFT 2024-08-21 421. 128.(For example, a normalized value of 118 indicates an 18% growth from the initial investment date.)
3. Plotting Side-by-Side with ggplot2
Because yahoofinancer returns a tidy, long-format tibble
with a symbol column, plotting multiple series side-by-side
with ggplot2 requires very little code. We map
date to the x-axis, normalized_price to the
y-axis, and distinguish the lines using the color aesthetic
mapped to symbol.
ggplot(portfolio_performance, aes(x = date, y = normalized_price, color = symbol)) +
geom_line(linewidth = 0.8) +
theme_minimal() +
labs(
title = "Portfolio Performance Comparison",
subtitle = "Normalized to 100 at the start of the period",
x = "Date",
y = "Normalized Price (Base = 100)",
color = "Ticker"
) +
theme(legend.position = "bottom")This chart instantly reveals the relative performance and volatility of the selected stocks over the given time horizon.
You now have a reusable workflow for comparing any set of tickers. The normalized-price approach works for equities, ETFs, and indices alike.
4. Minimal Reproducible Example
Below is the complete, self-contained workflow in a single copy-pasteable script:
library(yahoofinancer)
library(dplyr)
library(ggplot2)
# 1. Fetch prices for a portfolio of symbols
symbols <- c("AAPL", "MSFT", "GOOGL", "AMZN")
portfolio_prices <- yf_download_prices(tickers = symbols, interval = "1d")
# 2. Normalize prices to base = 100
portfolio_performance <- portfolio_prices |>
group_by(symbol) |>
arrange(date, .by_group = TRUE) |>
mutate(
date = as.Date(date),
normalized_price = (close / close[1]) * 100
) |>
ungroup()
# 3. Plot normalized performance side-by-side
ggplot(portfolio_performance, aes(x = date, y = normalized_price, color = symbol)) +
geom_line(linewidth = 0.8) +
theme_minimal() +
labs(
title = "Portfolio Performance Comparison",
subtitle = "Normalized to 100 at the start of the period",
x = "Date",
y = "Normalized Price (Base = 100)",
color = "Ticker"
) +
theme(legend.position = "bottom")5. Summary
In this guide, you learned how to:
-
Batch download prices: Retrieve historical series
for multiple tickers simultaneously using
yf_download_prices(). -
Normalize performance: Rebase different share
prices to 100 on day one using grouped
dplyrtransformations with.by_group = TRUE. -
Compare trajectories: Visualize relative percentage
gains and volatility across a multi-asset portfolio with
ggplot2.
6. Going Further
Now that you can track the performance of a portfolio, explore other
data features yahoofinancer offers:
Discover Trending Stocks: Automatically seed or expand your portfolio watchlist using
get_trending("US").-
Clean Large Universes: Use
validate()to filter out invalid or delisted symbols prior to bulk downloading: Fundamental Screening: Learn how to extract financial statements and valuation ratios across peers before analyzing prices—see
vignette("fundamental-screening", package = "yahoofinancer").Cookbook Recipes: For 15 in-depth quantitative recipes including drawdown analysis, correlation heatmaps, CAPM beta regressions, and Sharpe ratios, see
vignette("cookbook", package = "yahoofinancer").