← All posts

S&P 500 Additions: Does the Index Effect Still Exist?

Does the S&P 500 index effect still exist? An event study of 162 additions since 2016 finds no abnormal return at the rebalance close, only a pre-date run-up.

What's the question?

When S&P Dow Jones Indices adds a company to the S&P 500, every fund tracking the index must own it at the same weight by the same date. That demand is forced and price-insensitive, and the schedule is public in advance.

That mechanism produced the index effect: the abnormal gain of an added stock between announcement and inclusion. Greenwood and Sammon, in the Journal of Finance in April 2025, put the average addition return at 7.4% during the 1990s and 0.3% over the decade to 2020. Passive assets grew enormously since, so forced demand is larger than ever while the premium is supposed to have gone. The test below covers 2016 to 2024, and asks whether a pre-inclusion gain reverses.

The approach

The sample is every S&P 500 addition effective between 1 January 2016 and 31 December 2024: 193 events. Abnormal return means the stock’s daily return minus SPY’s on the same day, cumulated across a window, with beta fixed at 1.

One caveat governs everything below. The record dates the day membership took effect, not the announcement: Workday’s was announced on 6 December 2024 and took effect eleven trading days later, while ad-hoc replacements come about five days ahead. No pre-date window here is a clean announcement effect. How announcement and effective dates differ, and where the dated record of every change comes from, are set out in a guide to S&P 500 additions and deletions history.

  1. Pull dated additions from a point-in-time membership record; today’s constituent list omits additions later acquired or demoted.
  2. Align each event to trading days, with day 0 as the first day of membership. Volume verifies that convention: index funds trade the closing auction of the last day before membership starts.
  3. Validate identity before using any price series: the symbol must map to the same company on the effective date, and no window may hold a one-day move beyond 60%. TWTR belonged to Tweeter Home Entertainment before Twitter.
  4. Require a complete return series over [-40, +60] trading days, which drops 31 events: 23 spin-offs or merger-created share classes admitted on their first trading day, and 8 whose traded history does not span the window. Survivors number 90 of 105 events in 2016-2019 and 72 of 88 in 2020-2024, so attrition does not favour either era.

Code

import numpy as np
import pandas as pd
from scipy import stats
import xfinlink as xfl

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

PRE, POST, JUMP = 40, 60, 0.60
WINDOWS = [("[-40,-11]", -40, -11), ("[-10,-1]", -10, -1), ("[-1] only", -1, -1),
           ("[0]", 0, 0), ("[+1,+5]", 1, 5), ("[+1,+20]", 1, 20), ("[+1,+60]", 1, 60)]

ev = xfl.index_events("sp500", event_type="added",
                      start="2016-01-01", end="2024-12-31")
ev["eff"] = pd.to_datetime(ev["effective_date"])

spy = xfl.prices("SPY", start="2015-08-01", end="2025-06-30",
                 fields=["return_daily"])
cal = pd.DatetimeIndex(sorted(spy["date"].unique()))
spy_ret = spy.set_index("date")["return_daily"].astype(float)

# identity windows per symbol: resolve() takes 10 tickers per call
spans, tk_all = {}, sorted(ev["ticker"].unique().tolist())
for i in range(0, len(tk_all), 10):
    for t, payload in xfl.resolve(tk_all[i:i + 10])["data"].items():
        spans[t] = [(e["entity_id"], e.get("ticker_valid_from"))
                    for e in payload.get("entities", [])]

# batch by quarter, key the panel on entity_id rather than ticker
ev["q"] = ev["eff"].dt.to_period("Q")
panel = {}
for q, g in ev.groupby("q"):
    d = xfl.prices(sorted(g["ticker"].unique().tolist()),
                   start=(g["eff"].min() - pd.Timedelta(days=95)).strftime("%Y-%m-%d"),
                   end=(g["eff"].max() + pd.Timedelta(days=100)).strftime("%Y-%m-%d"),
                   fields=["return_daily", "volume"])
    for eid, de in d.groupby("entity_id"):
        panel[(q, eid)] = de.sort_values("date").set_index("date")

paths = []
for _, r in ev.iterrows():
    hit = [s for s in spans.get(r["ticker"], []) if s[0] == r["entity_id"]]
    if not hit or (hit[0][1] and pd.Timestamp(hit[0][1]) > r["eff"]):
        continue                 # symbol did not belong to this company yet
    d = panel.get((r["q"], r["entity_id"]))
    pos = cal.searchsorted(r["eff"])
    if d is None or pos - PRE < 0 or pos + POST >= len(cal) or cal[pos] != r["eff"]:
        continue
    win = cal[pos - PRE: pos + POST + 1]
    ret = d["return_daily"].astype(float).reindex(win)
    if ret.isna().any() or ret.abs().max() > JUMP:
        continue                 # no pre-history, a trading gap, or a data break
    vol = d["volume"].astype(float).reindex(win)
    paths.append({
        "era": "2016-2019" if r["eff"] < pd.Timestamp("2020-01-01") else "2020-2024",
        "ar": (ret - spy_ret.reindex(win)).values,
        "relvol_m1": vol.iloc[PRE - 1] / vol.iloc[:PRE - 1].median()})

res = pd.DataFrame([p["ar"] for p in paths], columns=np.arange(-PRE, POST + 1))
meta = pd.DataFrame([{k: v for k, v in p.items() if k != "ar"} for p in paths])

def car(df, a, b):
    return df.loc[:, [c for c in df.columns if a <= c <= b]].sum(axis=1) * 100

print(f"median day -1 volume vs days -40..-2: {meta['relvol_m1'].median():.1f}x")
for name, a, b in WINDOWS:
    v = car(res, a, b)
    t = stats.ttest_1samp(v, 0)
    print(f"{name:<11}{v.mean():8.2f}%{v.median():8.2f}%{t.statistic:9.2f}{t.pvalue:9.4f}")
    for era in ["2016-2019", "2020-2024"]:
        print("   ", era, f"{car(res[meta['era'] == era], a, b).median():6.2f}%")

Full script with formatting and visualisation: sp500-index-effect-event-study-python.py

Output

Median cumulative abnormal return against SPY from 40 trading days before to 60 trading days after S&P 500 inclusion, for additions in 2016-2019 and 2020-2024.
S&P 500 additions, effective 2016-01-01 to 2024-12-31: 193 events
Complete return history over [-40,+60] trading days: 162 events (31 excluded)
  2016-2019: 90 of 105 events kept (86%)
  2020-2024: 72 of 88 events kept (82%)

Excluded events (31):
  UA     2016-04-08  did not trade on 0 of 40 days before, 1 of 61 from day 0
  FTV    2016-07-05  did not trade on 40 of 40 days before, 1 of 61 from day 0
  FTI    2017-01-17  did not trade on 0 of 40 days before, 1 of 61 from day 0
  IIN    2017-04-05  listing history does not span the event window
  INFO   2017-06-02  listing history does not span the event window
  BHF    2017-08-07  did not trade on 40 of 40 days before, 1 of 61 from day 0
  IQV    2017-08-29  did not trade on 40 of 40 days before, 55 of 61 from day 0
  DD     2017-09-01  did not trade on 0 of 40 days before, 1 of 61 from day 0
  LIN    2018-10-31  did not trade on 40 of 40 days before, 1 of 61 from day 0
  DOW    2019-04-02  did not trade on 40 of 40 days before, 1 of 61 from day 0
  CTVA   2019-06-03  did not trade on 40 of 40 days before, 1 of 61 from day 0
  BMS    2019-06-07  did not trade on 0 of 40 days before, 59 of 61 from day 0
  AMCR   2019-06-11  did not trade on 40 of 40 days before, 1 of 61 from day 0
  PSKY   2019-12-23  listing history does not span the event window
  TI     2019-12-23  listing history does not span the event window
  CARR   2020-04-03  did not trade on 40 of 40 days before, 1 of 61 from day 0
  OTIS   2020-04-03  did not trade on 40 of 40 days before, 1 of 61 from day 0
  VNT    2020-10-09  did not trade on 40 of 40 days before, 1 of 61 from day 0
  VTRS   2020-11-17  did not trade on 40 of 40 days before, 1 of 61 from day 0
  AIRC   2020-12-15  did not trade on 40 of 40 days before, 1 of 61 from day 0
  OGN    2021-06-03  did not trade on 40 of 40 days before, 1 of 61 from day 0
  CEG    2022-02-02  did not trade on 40 of 40 days before, 1 of 61 from day 0
  WBD    2022-04-11  did not trade on 40 of 40 days before, 1 of 61 from day 0
  MBC    2022-12-15  did not trade on 40 of 40 days before, 1 of 61 from day 0
  GEHC   2023-01-04  did not trade on 40 of 40 days before, 1 of 61 from day 0
  FTRE   2023-07-03  did not trade on 40 of 40 days before, 1 of 61 from day 0
  VLTO   2023-10-02  did not trade on 40 of 40 days before, 1 of 61 from day 0
  SOLV   2024-04-01  did not trade on 40 of 40 days before, 1 of 61 from day 0
  GEV    2024-04-02  did not trade on 40 of 40 days before, 1 of 61 from day 0
  SW     2024-07-08  did not trade on 40 of 40 days before, 1 of 61 from day 0
  AMTM   2024-09-30  did not trade on 40 of 40 days before, 1 of 61 from day 0

Identity re-checked against continuous price history for 4 names: COO, IIN, AOS, TI

Largest single-day moves left in the sample (checked by name)
ticker        eff max_move  max_move_day
  EPAM 2021-12-14    45.7%            51
  NKTR 2018-03-19    41.8%            53
  SMCI 2024-03-18    35.9%           -40
   HWM 2020-04-03    26.4%            -8
   AMD 2017-03-20    24.2%            30

Day-0 alignment check (effective date = first day of membership)
  median volume on day -1 vs days -40..-2: 25.3x
  share of events whose heaviest volume day in [-5,+5] is day -1: 98%
  peak-volume day distribution: {-5: 1, -1: 158, 0: 1, 3: 1, 4: 1}

Cumulative abnormal return vs SPY, market-adjusted (beta = 1)
window          mean   median   t-stat        p   % > 0
[-40,-11]      6.54%    4.88%     5.84   0.0000     67%
[-10,-1]       1.73%    0.22%     3.09   0.0023     51%
[-1] only     -0.05%   -0.25%    -0.24   0.8123     44%
[0]           -0.00%   -0.08%    -0.02   0.9829     47%
[+1,+5]       -0.22%    0.16%    -0.69   0.4900     52%
[+1,+20]      -0.20%    0.39%    -0.30   0.7622     51%
[+1,+60]      -0.71%    1.14%    -0.56   0.5785     52%

By era (median CAR, mean in brackets)
window                  2016-2019             2020-2024
[-40,-11]         4.11% (  4.91%)       6.49% (  8.59%)
[-10,-1]         -1.08% ( -0.33%)       3.46% (  4.32%)
[-1] only        -0.42% ( -0.49%)       0.15% (  0.51%)
[0]               0.11% (  0.31%)      -0.32% ( -0.40%)
[+1,+5]           0.19% ( -0.29%)      -0.02% ( -0.14%)
[+1,+20]          0.79% (  0.46%)      -0.84% ( -1.02%)
[+1,+60]          0.55% ( -0.49%)       1.38% ( -1.00%)

Does the run-up reverse? (medians)
  2016-2019: [-10,-1] = -1.08%, [+1,+20] = +0.79%  ->  no run-up to reverse
  2020-2024: [-10,-1] = +3.46%, [+1,+20] = -0.84%  ->  gives back 24% of it

Largest pre-inclusion run-ups
ticker        eff  run_up   hold60
  MRNA 2021-07-21 29.4256   9.4904
  DXCM 2020-05-12 22.3826  -7.2565
  SMCI 2024-03-18 19.2331 -21.6056
  TWTR 2018-06-07 18.5090 -12.6304
Widest 60-day outcomes after inclusion
ticker        eff  run_up   hold60
  PLTR 2024-09-23 16.0064  66.8539
  ETSY 2020-09-21  0.1363  36.2518
  EPAM 2021-12-14  9.0427 -88.2098
  NKTR 2018-03-19 -1.6486 -53.1170

Median SPY return over the same [+1,+20] window: 1.71% (a beta error of 0.3 moves the estimate by 0.51pp)

What this tells us

Alignment first. For 158 of 162 events the heaviest volume day in [-5, +5] is day -1, where the median stock trades 25.3 times normal volume.

On that day the median abnormal return is -0.25%, the mean -0.05%, t-statistic -0.24. Buying large enough to lift volume twenty-five-fold clears at no measurable premium, and day 0 is flat at -0.08%.

The gain that survives testing sits far from the event: over [-40, -11] the median addition beats SPY by 4.88%, t-statistic 5.84, with 67% of events positive. That window closes before the announcement for quarterly additions, so it does not pay for index demand. It reflects how the index gets built: S&P screens on size, liquidity, float and recent profits, so a stock qualifies only after it has appreciated.

The era split holds a surprise: the close-in window did not decay but grew, from a median of -1.08% in 2016-2019 to +3.46% in 2020-2024. Who was added explains much of it, with Moderna up 29.4% before its July 2021 inclusion and Super Micro 19.2% before March 2024. The same cohort carries the wider [-40, -11] figure, pointing to selection rather than heavier late pressure.

Afterwards nothing reliable happens: every post-date window returns a p-value above 0.48. The recent cohort hands back 24% of its close-in run-up over the next 20 trading days, a median of -0.84%, while the earlier cohort has none to hand back. Means and medians disagree in sign over [+1, +60] because company news swamps index flow: EPAM lost 88.2% against SPY within 60 trading days of its December 2021 inclusion, once Russia invaded Ukraine and stranded a workforce concentrated there.

So what?

Treat the rebalance close as a liquidity event, not a price event. Volume at twenty-five times normal with a flat abnormal return means the auction absorbs size without moving price, which matters to anyone working a large order. What inclusion does to the shareholder register is measured in a 13F study of S&P 500 additions.

Buying an announced addition to sell into the rebalance has no support here beyond what momentum already provides. Fading them afterwards is weaker still: 0.84% over 20 days does not cover costs, and the tails are wide enough that sizing matters more. An event study of Nasdaq-100 changes runs the same test on a second index.

The measurement lesson generalises: rebuild the universe as it stood, confirm a symbol still means the same company on the event date, and read the exclusions before trusting an average.

Built with xfinlink — free financial data API for Python.