> ## Documentation Index
> Fetch the complete documentation index at: https://fruitstand.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Where to Get Trailing Returns for Every US Fund and ETF in Bulk

> Where to get trailing returns for every US mutual fund and ETF in bulk: licensed feeds, free SEC files, a Snowflake listing, or calculating them in SQL.

*By [Fruit Stand Team](https://fruitstand.dev/about.md) · November 2, 2026 · Guides*

Trailing returns for every US fund and ETF in bulk come from three places:
licensed data feeds from **Morningstar** and **CRSP**, the **SEC's free
data files**, which run months behind, or a **Snowflake Marketplace
listing**. Fruit Stand's Fund Returns listing has 1-day to 20-year returns
for about 32,000 US mutual funds and ETFs in one table, for \$250 a month.

You can also calculate them in bulk from adjusted prices, pulled for free
with the **yfinance** library or from a price dataset. That works for a
handful of funds, but getting it right for tens of thousands takes more care
than the formula suggests. This post covers each option and shows the SQL
for both.

> [!NOTE]
>
> Fruit Stand publishes the Fund Returns listing. Details for the other
> sources come from their own pages as of November 1, 2026. For a
> listing-by-listing comparison of fund data on the Snowflake Marketplace, see
> [Fund and ETF Returns Data on Snowflake, Compared](https://fruitstand.dev/blog/fund-etf-returns-comparison.md).

## The options at a glance

| Source | Coverage | Trailing periods | How current | Bulk delivery |
| - | - | - | - | - |
| [Fund Returns](https://app.snowflake.com/marketplace/listing/GZTYZTG3LLO9) (Fruit Stand) | \~32,000 US mutual funds and ETFs, including delisted funds | 1D, 1W, 1M, 3M, 6M, YTD, 1Y, 2Y, 3Y, 4Y, 5Y, 7Y, 10Y, 15Y, 20Y, since inception, plus calendar years | Recalculated as prices land, five days a week | Snowflake table for the whole universe, \$250/month, 30-day free trial |
| [Morningstar Direct](https://www.morningstar.com/business/products/direct) | Broad global fund coverage | Trailing total returns | Daily | Licensed data feed, by request |
| [CRSP US Mutual Fund Database](https://app.snowflake.com/marketplace/listing/GZ1MMZB2UP31/center-for-research-in-security-prices-llc-crsp-survivor-bias-free-us-mutual-fund-database) | 64,000+ funds, active and delisted | Monthly total returns, not trailing periods | Updated quarterly | Licensed database, by request |
| [SEC risk/return summary data](https://www.sec.gov/data-research/sec-markets-data/mutual-fund-prospectus-riskreturn-summary-data-sets) | Mutual funds and ETFs that file prospectuses in XBRL | 1, 5 and 10 years, plus since inception | Once a year per fund, as of the prior December 31 | Free quarterly ZIP files, since 2010 |
| [SEC Form N-PORT data](https://www.sec.gov/data-research/sec-markets-data/form-n-port-data-sets) | Registered mutual funds and ETFs (not money market funds), by share class | Monthly total returns, not trailing periods | Quarterly, about 60 days after each quarter ends | Free quarterly ZIP files, since late 2019 |
| [yfinance](https://github.com/ranaroussi/yfinance) (unofficial Yahoo Finance library) | Any ticker Yahoo lists; you supply the list | None ready-made; you calculate them from its adjusted prices | Daily prices | Free Python library; Yahoo throttles bulk requests, personal use only |

## Licensed data: Morningstar and CRSP

**Morningstar** is the standard source for fund performance in the
industry, and its trailing total returns are what most fact sheets and
advisor tools show. Access comes through [Morningstar Direct](https://www.morningstar.com/business/products/direct) or a licensed
data feed, priced by quote.

**CRSP's** [mutual fund database](https://app.snowflake.com/marketplace/listing/GZ1MMZB2UP31/center-for-research-in-security-prices-llc-crsp-survivor-bias-free-us-mutual-fund-database) is the standard in academic research. It's
survivorship-bias-free, with 64,000+ funds including delisted ones, but it
publishes monthly returns, not trailing periods. You'd compound the monthly
returns into 1-, 3- and 5-year figures yourself.

Both make sense when you already have an enterprise data budget, or need
the exact numbers a fund's own fact sheet uses.

## Free: the SEC's prospectus data

Every mutual fund prospectus has a risk/return summary with an average
annual total returns table: 1, 5 and 10 years, or since inception for
younger funds. Funds tag it in XBRL, and the SEC publishes the extracted
data as [quarterly ZIP files](https://www.sec.gov/data-research/sec-markets-data/mutual-fund-prospectus-riskreturn-summary-data-sets) going back to December 2010. ETFs that file the
same prospectus form are included.

It's free and comes straight from the funds, but it doesn't answer most
questions about current performance:

* **Once a year.** The table is as of the last calendar year end, updated
  when the fund files its annual prospectus. In November it can be ten
  months old.
* **Three periods.** There's no year-to-date, 3-year or 3-month figure.
* **Sales charges.** For share classes with a front-end load, the table's
  returns are after the maximum sales charge, so they'll be lower than a
  load-waived return.
* **Identifiers.** Rows are keyed by the SEC's series and class IDs. You
  have to map them to tickers to join your other data.

## Free: the SEC's N-PORT monthly returns

Funds also report returns every month. Registered funds other than money
market funds file Form N-PORT, which includes the fund's total return for
each of the previous three months, by share class and net of fees. The SEC
publishes the public filings as [quarterly data sets](https://www.sec.gov/data-research/sec-markets-data/form-n-port-data-sets), with the returns
in a `MONTHLY_TOTAL_RETURN` table, going back to late 2019.

That makes N-PORT the most current free source for the whole universe, but
it's monthly returns, not trailing ones. You compound them yourself. With
the returns loaded into a table of one row per share class per month, the
1-year return is:

```sql theme={null}
-- Example against your own table: monthly_returns(class_id, month, monthly_return).
-- N-PORT reports returns as percentages (1.23 for 1.23%), so divide by 100 when loading.
select
    class_id,
    exp(sum(ln(1 + monthly_return))) - 1 as return_1y
from monthly_returns
where month > dateadd('month', -12, (select max(month) from monthly_returns))
group by class_id
having count(*) = 12;
```

Before you rely on it:

* **Months behind, not days.** Only the quarter-end filing is public, and it
  appears 60 days after the quarter ends. The SEC then publishes the data
  sets quarterly, so the latest month can be three to five months old.
* **Short history.** The data starts in late 2019, so there are no 10-year
  returns until late 2029.
* **Class IDs, not tickers.** Returns are keyed by the SEC's share class ID,
  so you map each class to its ticker before joining other data.
* **Month-end only.** There's no year-to-date or 1-week figure between month
  ends.

Free lookup tools aren't bulk sources. [FINRA's Fund Analyzer](https://tools.finra.org/fund_analyzer/),
for example, shows current 1-, 3-, 5- and 10-year returns, but one fund at a
time on a website, with no download.

## Free but unofficial: yfinance

[yfinance](https://github.com/ranaroussi/yfinance) is an open-source Python
library that downloads prices from Yahoo Finance. It's the most common free
way to get fund prices in bulk, and with adjusted prices you can calculate
trailing returns yourself:

```python theme={null}
import pandas as pd
import yfinance as yf

# Adjusted closes (dividends and splits applied) for each fund, one column per ticker.
prices = yf.download(["VTI", "VFIAX", "SCHD"], period="5y", auto_adjust=True)["Close"]

as_of = prices.index[-1]
return_1y = prices.iloc[-1] / prices.asof(as_of - pd.DateOffset(years=1)) - 1
```

For three funds, that's fine. For every US fund and ETF, Yahoo becomes the
limit:

* **Throttling.** Yahoo doesn't publish rate limits, and it throttles by IP
  address. Pull tens of thousands of tickers and requests start failing with
  "Too Many Requests" (`YFRateLimitError`). Users report blocks lasting
  hours, sometimes more than a day. A full backfill means batching, caching
  and pacing requests over days.
* **Yahoo's quirks.** yfinance isn't an official API. It reads Yahoo's
  website endpoints, which change without notice, and each change breaks the
  library until a new release ships. Data quirks show up too: missing or
  repeated days, and adjusted history that shifts between pulls.
* **No fund list.** There's no call for "every US mutual fund". You need
  your own list of \~32,000 tickers and have to keep it current as funds
  launch, close and merge.
* **Personal use only.** yfinance says it isn't affiliated with Yahoo and
  that Yahoo's API is intended for personal use only. That rules it out for
  client work or a shared company warehouse.

## Calculating trailing returns yourself

A trailing return is the change in adjusted price over a window ending on
an as-of date:

```text theme={null}
cumulative = end_adjusted_close / base_adjusted_close - 1
annualized = (end_adjusted_close / base_adjusted_close) ^ (365.25 / days) - 1
```

With daily adjusted prices in Snowflake, for example from the
[Historical Stock Prices listing](https://fruitstand.dev/datasets/historical-stock-prices.md), an
`ASOF JOIN` finds each window's base price, the last trading day on or
before the start of the window:

```sql theme={null}
with px as (
    select code, date, adjusted_close
    from security_price_history_vw
    where exchange_code = 'US'
      and code in ('VTI', 'VFIAX', 'SCHD')
),
latest as (
    select code, max(date) as as_of from px group by code
),
windows as (
    select code, as_of, '1y' as period, dateadd('year', -1, as_of) as window_start from latest
    union all
    select code, as_of, '5y', dateadd('year', -5, as_of) from latest
)
select
    w.code,
    w.period,
    b.date as base_date,
    case
        when datediff('day', b.date, w.as_of) >= 730
            then power(e.adjusted_close / b.adjusted_close, 365.25 / datediff('day', b.date, w.as_of)) - 1
        else e.adjusted_close / b.adjusted_close - 1
    end as trailing_return
from windows w
asof join px b
    match_condition (w.window_start >= b.date)
    on w.code = b.code
join px e
    on e.code = w.code and e.date = w.as_of
order by w.code, w.period;
```

That's correct for three funds. At 32,000 funds, these are the things that
go wrong:

* **Adjusted prices change.** Every dividend restates a fund's entire
  adjusted history. Mutual funds that pay monthly restate it monthly, so
  stored returns go stale unless you recompute from a full reload. See
  [Adjusted Close vs. Close](https://fruitstand.dev/blog/adjusted-close-vs-close.md) for why.
* **Funds younger than the window.** A fund launched 3 years ago has no
  5-year return. Without a check, the `ASOF JOIN` finds no base price and
  the row quietly drops out.
* **Thinly traded funds.** When a fund doesn't trade for a few days, the
  base price comes from an earlier date than the window implies. Keep
  `base_date` next to the return so it's visible.
* **When to annualize.** By convention, periods under a year are never
  annualized. Annualizing a 3-month return turns a good quarter into an
  implausible annual figure.
* **NAV vs. market price.** For mutual funds, the daily price is the NAV.
  For ETFs it's the market close, so the return is a market-price return.
  An ETF's fact sheet usually shows both, and they differ slightly.
* **Delisted funds.** Funds close and merge every month. If your fund list
  only has today's funds, any history you build has survivorship bias.

## The ready-made version

Fund Returns does that work in its pipeline. A fund's full price history
reloads whenever it posts a dividend or split, returns are recalculated as
new prices land, with a full rebuild twice a week, and delisted funds stay
in the tables with their history. Every fund's latest returns are one query:

```sql theme={null}
select f.code, f.type, r.as_of_date,
       r.return_ytd, r.return_1y, r.return_5y, r.return_10y
from ext_fund_trailing_returns_vw r
join ext_fund_vw f
    on f.code = r.code
where r.code in ('VTI', 'VFIAX', 'VBTLX')
qualify row_number() over (partition by r.code order by r.as_of_date desc) = 1
order by r.code;
```

Run on October 30, 2026, it returned:

| code | type | as\_of\_date | return\_ytd | return\_1y | return\_5y | return\_10y |
| - | - | - | -: | -: | -: | -: |
| VBTLX | FUND | 2026-10-30 | -0.004617 | 0.001193 | -0.001720 | 0.014818 |
| VFIAX | FUND | 2026-10-30 | 0.117181 | 0.123552 | 0.120059 | 0.153817 |
| VTI | ETF | 2026-10-30 | 0.117753 | 0.124916 | 0.109450 | 0.148737 |

Returns are decimals, so 0.148737 is 14.87%. Periods of 2 years and longer
are annualized, and shorter ones, including year-to-date, are cumulative.
Remove the `where` clause and the same query returns every fund and ETF.
Calendar-year returns are in a separate table, one row per fund per year.
The full schema and the return formulas are in the
[Fund Returns docs](https://fruitstand.dev/datasets/fund-returns.md).

## How close are the numbers?

Every method here starts from adjusted prices, so it's worth checking them
against the funds' own published returns. Here are Vanguard's figures for
two of its largest funds as of September 30, 2026, next to Fund Returns and
a yfinance calculation for the same date:

| Fund | Source | 1Y | 3Y | 5Y | 10Y |
| - | - | -: | -: | -: | -: |
| VTI | Vanguard (market price) | 15.35% | 22.34% | 12.56% | 14.70% |
| VTI | Fund Returns | 15.34% | 22.30% | 12.56% | 14.70% |
| VTI | yfinance | 15.34% | 22.33% | 12.56% | 14.70% |
| VFIAX | Vanguard | 15.69% | 22.84% | 13.74% | 15.29% |
| VFIAX | Fund Returns | 15.68% | 22.81% | 13.74% | 15.28% |
| VFIAX | yfinance | 15.68% | 22.84% | 13.74% | 15.28% |

All three agree to within 0.04 percentage points. The small gaps come from
rounding and from how each source picks the base date for a window. For
VTI, compare against Vanguard's market-price return: an ETF's daily close is
a market price, and its NAV return differs slightly (15.33% over 1 year).

## Exporting the whole universe

If what you need is a file, for a spreadsheet, a notebook or another
system, the listing gets you there with one statement. In your own
Snowflake account, unload the latest row for every fund to a stage:

```sql theme={null}
create stage if not exists fund_exports;

copy into @fund_exports/trailing_returns/
from (
    select *
    from ext_fund_trailing_returns_vw
    qualify row_number() over (partition by code order by as_of_date desc) = 1
)
file_format = (type = csv compression = gzip)
header = true
overwrite = true;
```

Download the files with `GET` from SnowSQL or the Snowflake CLI, or point the
stage at your own S3, Azure or GCS bucket instead. Wrap the statement in a
scheduled task and the export stays current after each refresh.

## Which should you use?

* **You need the exact figures on a fund's fact sheet:** license
  Morningstar data.
* **You're doing academic research on fund performance:** CRSP, through
  your institution.
* **You need current trailing returns for the whole universe in SQL:** a
  Marketplace listing such as [Fund Returns](https://fruitstand.dev/datasets/fund-returns.md).
* **You want free data for the whole universe and can wait a few
  months:** the SEC's N-PORT monthly returns, compounded yourself.
* **You're running a personal project on a few dozen funds:** yfinance,
  with caching, within Yahoo's terms.
* **You need a custom period or benchmark:** calculate from adjusted
  prices, such as the [Historical Stock Prices](https://fruitstand.dev/datasets/historical-stock-prices.md)
  listing's, and plan for the issues above.

## F.A.Q.

> [accordion] What's the difference between trailing and calendar-year returns?
>
> A trailing return covers a fixed window ending on a date, such as the 1-year return as of today. A calendar-year return covers January 1 to December 31 of a given year. Trailing returns change every day; calendar-year returns only change for the current year.

> [accordion] Are trailing returns annualized?
>
> Periods of more than a year usually are, as a compound annual growth rate, so a 5-year and a 10-year return can be compared directly. Periods of a year or less, including year-to-date, are cumulative. Fund Returns annualizes periods of 2 years or longer.

> [accordion] Do trailing returns include dividends?
>
> They should. Returns calculated from adjusted prices include reinvested dividends and distributions, which makes them total returns. Returns from raw closing prices leave out the income and understate performance, especially for bond and dividend funds.

> [accordion] Why don't my numbers match the fund's fact sheet exactly?
>
> Small differences usually come from the as-of date, NAV versus market price for ETFs, whether sales charges are deducted, and how the provider handles distributions. A fact sheet's standardized returns are often month-end or quarter-end, not today.

> [accordion] Can I download trailing returns for every fund as a file?
>
> From the Fund Returns listing, yes. It's a table in your Snowflake account, so `COPY INTO` a stage writes the whole universe to CSV or Parquet in one statement (see [Exporting the whole universe](#exporting-the-whole-universe)), and a scheduled task can repeat it after each refresh. The SEC's prospectus data is also downloadable, as quarterly ZIP files.

> [accordion] Can I use yfinance to get returns for every mutual fund?
>
> Not reliably. yfinance downloads one price history per ticker from Yahoo, and Yahoo throttles bulk requests by IP, so pulling \~32,000 funds runs into "Too Many Requests" errors and multi-hour blocks. It also needs your own list of fund tickers, and Yahoo's API is for personal use only.

> [accordion] Is there free trailing return data for all US funds?
>
> Not current data. The SEC's N-PORT data sets have free monthly returns for every registered fund, but they run three to five months behind and you compound the trailing periods yourself. The SEC's prospectus data only updates once a year, as of December 31, for 1, 5 and 10 years. FINRA's Fund Analyzer is free and current, but you look up funds one at a time. The [Fund Returns listing](https://app.snowflake.com/marketplace/listing/GZTYZTG3LLO9) has a free 30-day trial.

To get trailing and calendar-year returns for every US fund and ETF in bulk,
start a free 30-day trial of the
[Fund Returns listing on the Snowflake Marketplace](https://app.snowflake.com/marketplace/listing/GZTYZTG3LLO9).
It covers about 32,000 US mutual funds and ETFs, including delisted funds,
with 16 trailing periods, for \$250/month after the trial. The schema and formulas are in the
[dataset docs](https://fruitstand.dev/datasets/fund-returns.md). Questions? Reach out at
[contact@fruitstand.dev](mailto:contact@fruitstand.dev).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.