The longer you hold a stock, the less likely you are to beat the market
Buffett once wrote that his favourite holding period is forever, so I test whether holding longer raises the odds of beating the index. It does the opposite. Across every US stock from 2000 to 2026, 45.7 percent beat the S&P 500 over one year, and only 27.0 percent beat it over fifteen.
Concentration is the likely explanation. Bessembinder (2018) finds that a small number of big winners account for most of the US market’s long-run return, and a cap-weighted index, which weights each stock by its market value, puts more and more weight on them as they grow. For a stock picked at random, the longer the holding period, the more likely the index is ahead because of those few names.
The test
- Universe. All US common stocks on NYSE and Nasdaq, active and delisted, 2000 to 2026.
- Entry. Every month-end is an entry date.
- Horizons. 1, 3, 5, 7, 9, 11, 13, and 15 years. For each entry I measure the stock’s total return, the price change plus reinvested dividends, to the last trading day on or before the anniversary.
- Benchmark. The S&P 500 total return over the identical window.
- Beat. The stock’s forward return exceeds the S&P 500’s over the same dates.
- Delisted stocks. Kept in the sample. At delisting I take the last price and compound it at a flat 3% a year, a Treasury-bill proxy, to the end of the horizon, following Bessembinder (2018).
Starting point
The test runs off one file of daily total returns for every US stock. Building it is a separate job that depends on the data vendor. I pull from LSEG Workspace. Because the data is licensed, the raw file stays off this page, and I show the code without running it here. The benchmark is the S&P 500 total return over each window.
Step 1. A cumulative return index per stock
To measure the return between any two dates, I first build a cumulative total-return index for each stock, starting at 1.0 on its first trading day. The forward return over any window is then the ratio of the index at the two ends.
# daily holds one row per stock-day, and ret is that day's total return. Adding 1 turns
# a return into a gross return, which is the form that multiplies.
daily["gross"] = 1 + daily["ret"]
# cumprod() multiplies the gross returns together inside each stock, taking the rows in
# the order they appear in the table, so they have to arrive in date order. Every value is
# then what 1.0 invested on that stock's first trading day has grown to by that date.
daily["cum_ret"] = daily.groupby("Instrument")["gross"].cumprod()
# pivot_table reshapes the long table into a wide one: rows are dates, columns are
# stocks, values the cumulative index. A stock with no price on a date is missing there.
cum_ret_wide = daily.pivot_table(index="Date", columns="Instrument", values="cum_ret")
# Each stock's last trading day and last index level, for the delisting fill below.
# last() on a group takes that group's final value and skips the missing ones.
last_trade_date = daily.groupby("Instrument")["Date"].max()
last_index_level = daily.groupby("Instrument")["cum_ret"].last()The forward return from entry to exit is cum_ret at exit divided by cum_ret at entry, so one index per stock prices every horizon at once.
Step 2. Keep the delisted stocks
Dropping the stocks that delisted would cause survivorship bias, counting only the stocks that survived: the names that disappear are mostly failures, so cutting them would raise the long-horizon beat rate. So I keep them. When a stock stops trading before the exit date, I take its last observed index level and compound it at that flat rate to the exit, following Bessembinder (2018). A bankrupt stock therefore books its loss and then earns cash-like returns for the rest of the horizon, while the index keeps compounding.
RF_ANNUAL = 0.03 # a flat T-bill proxy for delisted proceeds
def forward_return(level_at_entry, exit_date):
"""Each stock's total return from entry to exit, keeping delisted names."""
# level_at_entry has one index level per stock, for the stocks trading at entry.
# reindex puts the exit date's row on those same stocks in the same order, and a stock
# with no index level on the exit date comes back missing.
level_at_exit = cum_ret_wide.loc[exit_date].reindex(level_at_entry.index)
gone = level_at_exit.isna() # stopped trading before the exit date
delisted = level_at_exit.index[gone] # the names of those stocks
# A delisted stock keeps the last index level it reached and then earns the flat rate in
# RF_ANNUAL for the years between its last trade and the exit date (Bessembinder).
days_since_last_trade = (exit_date - last_trade_date.loc[delisted]).dt.days
years_since_last_trade = days_since_last_trade / 365.25
bill_growth = (1 + RF_ANNUAL) ** years_since_last_trade
# .loc aligns both sides on the stock name, so only the delisted rows change
level_at_exit.loc[delisted] = last_index_level.loc[delisted] * bill_growth
# Dividing the two index levels gives the total return over the window
return level_at_exit / level_at_entryStep 3. Did the stock beat the market over the exact window?
For each month-end entry and each horizon, I find the last trading day on or before the anniversary that many years later, take the stock’s forward return and the S&P 500’s over the same two dates, and record which won. Then I pool every stock-entry observation and take the share that beat the index.
HORIZONS = [1, 3, 5, 7, 9, 11, 13, 15] # holding horizons, in years
# The dates of the wide table are the days the market traded. resample("ME") groups them
# by calendar month and last() keeps the final trading day of each one, which is the entry
# date for that month.
trading_days = cum_ret_wide.index.to_series()
month_ends = trading_days.resample("ME").last()
def beat_rate(horizon_years):
"""Share of stock-entry observations that beat the S&P 500 over one holding horizon."""
hits = 0
total = 0
# Every month-end is one entry date, and the two counters accumulate across all of them.
# That is why this stays a loop: each month adds its stocks to the same pool.
for entry_date in month_ends:
# DateOffset gives the same calendar day that many years later. asof() then
# steps back to the last trading day on or before it, because the anniversary
# itself can be on a weekend or a holiday.
anniversary = entry_date + pd.DateOffset(years=horizon_years)
exit_date = cum_ret_wide.index.asof(anniversary)
# dropna() leaves only the stocks that had an index level on the entry date
level_at_entry = cum_ret_wide.loc[entry_date].dropna()
stock_return = forward_return(level_at_entry, exit_date)
benchmark_return = spx.asof(exit_date) / spx.asof(entry_date)
# A missing return is never greater than the benchmark, so it cannot count as a
# win, and total counts only the stocks whose return could be measured.
hits += (stock_return > benchmark_return).sum()
total += stock_return.notna().sum()
return hits / total * 100Here spx is the S&P 500 total-return index, priced over the identical dates so the comparison is like for like. Monthly entry dates give many overlapping windows, so each horizon rests on a large number of observations.
Results
Horizon % of stocks that beat the S&P 500
1 year 45.7%
3 years 42.4%
5 years 41.1%
7 years 38.5%
9 years 35.0%
11 years 31.6%
13 years 28.8%
15 years 27.0%
At one year, 45.7 percent of stocks beat the market, close to half. The share decreases at every step: 41.1 percent at five years, 35.0 percent at nine, and 27.0 percent at fifteen.

At no horizon does a majority of stocks beat the market. The likely cause is the concentration named in the opening: the median stock, the one in the middle when all stocks are ranked by return, cannot match the compounding of the few big winners that the index holds more and more of, so over long horizons most stocks underperform the index even when the index itself does well.
Limitations
Overlapping windows. Monthly entry dates make adjacent observations share almost all of their holding period, so they are not independent draws, and I do not attach a confidence interval, a range that likely contains the true value, to any figure.
A constant rate for cash. I compound delisted proceeds at a flat 3 percent a year. The real Treasury-bill rate, the interest on short-term US government debt, moved over the period, and I do not re-run the test with a time-varying rate.
The last price at delisting. I assume a delisted stock pays out its last traded price and then earns bills. Some bankruptcies pay less and some acquisitions pay a premium above the last print, so the delisting leg is an approximation.
The beat rate does not measure size. This counts how often a single stock beats the index, without measuring by how much. The winners can beat it by a wide margin, which is why a diversified index does well even though most of its members underperform it.
Conclusion
Holding a single stock for longer lowered the odds it beat the market. At one year nearly half of US stocks beat the S&P 500, and by fifteen years only about one in four did. The likely cause is concentration: a few big winners account for most of the index’s return, and a cap-weighted index holds more of them as they grow, so the longer the holding period, the more likely a typical stock is to trail the index. Buffett was describing great businesses chosen with care, and holding a stock picked at random is a different bet. The takeaway is that holding longer does not raise a single stock’s odds against the index, because without selection the extra time mostly favours the few winners already inside it.
Read next
- About 5% of stocks created all the net wealth The same few winners, measured as their share of the net wealth the market created.
- The most profitable US companies beat the S&P 500, and the least profitable lost money If most single stocks trail the index, which ones to avoid.
Disclaimer: a simplified, descriptive historical study for discussion purposes only. Not investment advice. Past performance does not guarantee future returns.
Data: US equity daily total returns from LSEG Workspace (licensed); the benchmark is the S&P 500 total return. I do not reproduce the raw file here.