← All posts

New vs Veteran S&P 500 Members: Returns by Tenure

S&P 500 members in their first year in the index returned 4.95% a year from 2000 to 2024, against 10.75% for members of 10+ years. Today’s list flips the order.

Companies in their first year in the S&P 500 returned 4.95% a year from 2000 to 2024, held equal-weight from the roster in force each month, while members of more than ten years returned 10.75%. Rebuilt from today’s list and its join dates, the order reverses: first-year members return 13.38% a year and veterans 12.28%.

Do companies joining the S&P 500 keep up with long-standing members?

This question concerns the years after a company joins, not the trading days around inclusion, which an event study of the S&P 500 index effect covers. Jeremy Siegel and Jeremy Schwartz, in the Financial Analysts Journal in 2006, found that the original 1957 members, bought and held, beat the continually updated index, which suggests later arrivals may lag.

Tenure means the years since the start of a company’s current membership. Measuring returns by tenure needs the companies that joined and later left, with join dates as they stood; today’s list keeps only additions still in the index, the plainest form of survivorship bias.

Sorting 300 monthly rosters by years in the index

  1. On each month-end trading day from December 1999 to November 2024, draw the roster in force with xfl.index("sp500", as_of=...), described in the index membership reference. Each row’s added_date starts the member’s current spell, so the 26 securities re-added within the period start again at zero, as the committee treats them as new additions.
  2. Split each roster into four tenure buckets (first year, years 2-3, years 4-10, more than 10 years) and hold each equal-weight the next month on monthly total returns. A member whose last day in the index is the formation date is not held (103 member-months); a recorded delisting return is added in the exit month (430 exits); a member with no return in the holding month drops out of that month (9 member-months).
  3. Compare each bucket with SPY, dividends included, and take the monthly first-year minus 10+ spread with its t-statistic.
  4. Repeat on today’s roster (3 November 2026), counting each member from its current join date.
  5. As a cross-check, follow every addition from 2000 to 2021 for 12 and 36 months after its join month, in the index or not. Each is compared with SPY over the same months, through its last return month if it stopped trading sooner; windows with a gap in monthly returns are left out.

Code

import numpy as np
import pandas as pd
import xfinlink as xfl

xfl.set_api_key("YOUR_API_KEY")  # free at https://xfinlink.com/signup

# Month-end trading days and SPY's monthly total return
spy = xfl.prices("SPY", start="1999-12-01", end="2024-12-31", fields=["return_daily"])
spy["date"] = pd.to_datetime(spy["date"])
month_ends = spy.groupby(spy["date"].dt.to_period("M"))["date"].max()
spy_m = spy.groupby(spy["date"].dt.to_period("M"))["return_daily"].apply(
    lambda r: (1 + r.fillna(0)).prod() - 1)
formation = month_ends.loc[:"2024-11"]

# The roster in force at each month end; added_date starts each member's current spell
pit = pd.concat([xfl.index("sp500", as_of=d.strftime("%Y-%m-%d")).dropna(subset=["entity_id"])
                 .assign(month=m, asof=d) for m, d in formation.items()])
today = xfl.index("sp500").dropna(subset=["entity_id"])
for f in (pit, today):
    f["entity_id"] = f["entity_id"].astype(int)
    f["added_date"] = pd.to_datetime(f["added_date"])
pit = pit[pd.to_datetime(pit["removed_date"]) != pit["asof"]]   # leaving on the formation date
surv = pd.concat([today[today["added_date"] <= d].assign(month=m, asof=d)
                  for m, d in formation.items()])               # survivor-only version

# Monthly total returns by entity id, with recorded delisting returns in the exit month
ids = sorted(set(pit.entity_id) | set(today.entity_id))
parts, exits = [], {}
for i in range(0, len(ids), 16):   # 16 ids fit on one 5,000-row page, so no page's exits are lost
    p = xfl.prices(entity_id=ids[i:i + 16], start="1999-11-01", end="2024-12-31",
                   interval="1mo", fields=["return_daily"], max_rows=100_000)
    assert len(p) < 5000
    parts.append(p)
    exits.update(p.attrs.get("meta", {}).get("delisting", {}) or {})
px = pd.concat(parts)
ret = px.assign(month=px["date"].dt.to_period("M")).pivot_table(
    index="month", columns="entity_id", values="return_daily", aggfunc="first")
for eid, ev in exits.items():
    eid, dl, m = int(eid), ev.get("delisting_return"), pd.Period(ev["event_date"], "M")
    if dl is not None and eid in ret.columns and m in ret.index:
        r0 = ret.at[m, eid]
        ret.at[m, eid] = (1 + (0 if pd.isna(r0) else r0)) * (1 + dl) - 1

# Tenure buckets at each month end, held equal-weight the next month
bins, labels = [0, 1, 3, 10, np.inf], ["first year", "years 2-3", "years 4-10", "10+ years"]
def bucket_returns(book):
    b = book.assign(hold=book["month"] + 1,
                    tenure=(book["asof"] - book["added_date"]).dt.days / 365.25)
    b["bucket"] = pd.cut(b["tenure"], bins, right=False, labels=labels)
    b["r"] = ret.stack().reindex(pd.MultiIndex.from_arrays([b["hold"], b["entity_id"]])).to_numpy()
    return b.dropna(subset=["r"]).groupby(["hold", "bucket"], observed=True)["r"].mean().unstack()

for name, book in [("point-in-time", pit), ("today's roster", surv)]:
    out = bucket_returns(book)
    ann = (1 + out).prod() ** (12 / len(out)) - 1
    spread = out["first year"] - out["10+ years"]
    t = spread.mean() / spread.std() * np.sqrt(len(spread))
    print(name, ann.round(4).to_dict(), f"spread {spread.mean() * 1200:+.2f} t={t:.2f}")

# Cross-check: each addition 2000-2021, 12 and 36 months after its join month, minus SPY over
# the same months (through the stock's last return month); windows with a gap are left out
def forward(eid, start, n):
    win = pd.period_range(start + 1, periods=n, freq="M")
    r = ret[eid].reindex(win)
    last = r.last_valid_index()
    if last is None or r.loc[:last].isna().any():
        return np.nan, np.nan
    return (1 + r.loc[:last]).prod() - 1, (1 + spy_m.loc[win[0]:last]).prod() - 1

for name, book in [("point-in-time", pit), ("today's roster", today)]:
    adds = book.drop_duplicates(["entity_id", "added_date"])
    adds = adds[adds["added_date"].between("2000-01-01", "2021-12-31")]
    for n in (12, 36):
        ex = pd.Series([np.subtract(*forward(e, pd.Period(a, "M"), n))
                        for e, a in zip(adds["entity_id"], adds["added_date"])]).dropna()
        print(name, n, len(ex), f"median {ex.median():.1%}", f"beat SPY {(ex > 0).mean():.1%}")

Full script with formatting and visualisation: sp500-new-members-vs-veterans-returns-python.py

Output

Annualised equal-weight return of S&P 500 members by tenure, 2000 to 2024: on point-in-time rosters it rises from 4.9% in the first year to 10.8% after ten years; on today’s roster every bucket returns 12.3% to 14.4%; SPY 7.6%
300 month ends, 1098 securities, holding months 2000-01 to 2024-12
Member-months held: 150,409; not held because the member left on the formation date: 103; no return in the holding month: 9; recorded delisting returns applied: 430
Securities re-added after an earlier spell (tenure restarts): 26

Point-in-time rosters: average members per month first year 24, years 2-3 48, years 4-10 131, 10+ years 298
             % a year  volatility %  months beating SPY %  excess vs SPY % a year  t-stat
first year       4.95         20.71                 49.00                   -1.52   -0.73
years 2-3        8.46         21.49                 52.33                    1.96    0.96
years 4-10       9.49         18.96                 52.00                    2.37    1.67
10+ years       10.75         16.71                 56.00                    3.13    2.60
all members      9.80         17.68                 52.67                    2.43    2.11 

Today's roster, today's join dates: average members per month first year 11, years 2-3 22, years 4-10 69, 10+ years 201
             % a year  volatility %  months beating SPY %  excess vs SPY % a year  t-stat
first year      13.38         20.05                 53.33                    6.10    2.49
years 2-3       13.82         19.54                 59.00                    6.40    3.34
years 4-10      14.36         17.54                 60.33                    6.49    5.02
10+ years       12.28         15.63                 60.00                    4.33    3.89
all members     12.93         16.10                 63.67                    4.99    4.77 

Spread: first year minus 10+ years, equal weight
  Point-in-time rosters: -4.65 points a year, t-stat -1.95, first year ahead in 48% of 300 months
  Today's roster, today's join dates: +1.77 points a year, t-stat 0.71, first year ahead in 55% of 300 months
  by five-year period, points a year:
                Point-in-time rosters  Today's roster, today's join dates
holding months                                                           
2000-2004                       -8.56                               10.54
2005-2009                       -6.48                               -6.19
2010-2014                       -1.89                                1.51
2015-2019                       -3.99                                2.76
2020-2024                       -2.31                                0.22
SPY: 7.62% a year

Additions 2000-2021: return from the end of the join month, minus SPY over the same months
(through the stock's last return month when it stopped trading sooner)
                                              additions  left out (gap)  median excess %  mean excess %  beat SPY %
group                              horizon                                                                         
Additions on point-in-time rosters 12 months        544               0             -4.6           -2.3        43.6
                                   36 months        544               1             -6.7           -3.0        45.3
Additions still in today's roster  12 months        242               0             -1.9            2.7        47.9
                                   36 months        242               0              9.3           11.4        55.4

Additions 2000-2021 no longer in today's roster, gap-free windows: 292; five largest 36-month losses
ticker          entity_name      added  36-month return %
   MIR          MIRANT CORP 2001-04-03              -99.1
  PALM             PALM INC 2000-07-28              -97.9
  BVSN      BROADVISION INC 2000-11-06              -97.7
  JDSU  J D S UNIPHASE CORP 2000-07-27              -97.5
  CNXT CONEXANT SYSTEMS INC 2000-01-31              -96.3

Newcomers trailed veterans on the dated rosters in every five-year stretch

On the dated rosters, return rises with tenure at every step, from 4.95% a year in the first year to 10.75% after ten years. The first-year bucket is the only one behind SPY, and more volatile than veterans, 20.71% against 16.71%. The newcomer spread of -4.65 points a year has a t-statistic of -1.95, close to the 5% threshold on its own, but it is negative in all five five-year periods, from -8.56 points in 2000-2004 to -1.89 in 2010-2014. Followed one by one, the median addition trailed SPY by 4.6 percentage points over 12 months and 6.7 over 36.

Today’s roster puts first-year members 1.77 points a year ahead of veterans, and its additions beat SPY by a median 9.3 percentage points over 36 months.

The distortion lands on newcomers. Switching to today’s list lifts the first-year bucket by 8.43 points a year and the 10+ bucket by 1.53. The survivor version holds 11 first-year members a month against 24, but two in three veterans (201 of 298). The missing newcomers are additions later removed, acquired or failed: Mirant lost 99.1% in the 36 months after joining in April 2001, and Palm 97.9% after joining in July 2000.

What a tenure signal needs before it reaches a backtest

On the dated rosters, buying new S&P 500 members and holding them through their first year has trailed both SPY and the index’s own veterans since 2000.

Because the survivor-only test reverses the newcomer spread, any signal built on time since inclusion has to be measured on the roster in force at each date. The historical S&P 500 constituents lookup lists the members on any past date, and a survival analysis of S&P 500 membership length examines how exit risk changes with years served.

The all-member portfolio itself beat SPY by 2.43 points a year, so part of the veterans’ 3.13-point excess is shared by every equal-weighted member portfolio; comparing buckets with one another removes it.

Built with xfinlink — free financial data API for Python.