Why does a backtest on today's index look so good?
Here is the most consequential data mistake a beginner makes, written as a recipe:
- Fetch the current members of a well-known index.
- Pull ten years of price history for those names.
- Test an idea on it.
- Be impressed.
Every step is reasonable. The result is worthless, and the reason is in step 1.
You selected on the outcome
Every company in today's constituent list has a property you did not ask for: it is still good enough to be in the index today. And the list is not even a fixed set of survivors — some of today's members were added part-way through your ten years and have no history at the start of the window at all. The companies that went bankrupt, were taken over, or shrank below the threshold and were dropped are not in your list — so they are not in your data at all.
You did not test an idea against the market of ten years ago. You tested it against a portfolio chosen with knowledge of who would still be standing. That is survivorship bias, and it is not a small correction. It flatters returns, suppresses measured drawdowns, and makes almost any strategy look competent.
A second, distinct problem rides along with it. Index membership is decided by a committee or a rule, on a date. Using today's membership for a past date credits your past self with a decision that had not yet been made — that is look-ahead bias. The two are different mistakes with the same cause, and it is worth keeping them apart, because fixing one does not fix the other.
The fix is a block in the same response
The components endpoint returns HistoricalTickerComponents alongside the current list. Each row carries Code, Name, StartDate, EndDate, IsActiveNow and IsDelisted. The specification's own example row reads:
AAPL / Apple Inc / StartDate 1982-11-30 / EndDate "" / IsActiveNow 1 / IsDelisted 0
An empty EndDate means membership has not ended. A populated one means it ended on that date. Which gives you a point-in-time membership query that is one line of logic:
A company was a member on date D if StartDate ≤ D and (EndDate is empty or EndDate ≥ D).
With one limit worth knowing before you trust the answer. The block carries exactly one row per company — 822 rows for 822 distinct codes on a live pull — and membership is not always one unbroken spell. Companies get deleted and later re-added, and a single StartDate/EndDate pair cannot express the gap in between. For such a name this test returns member right through the years it was out, which is a smaller version of the same look-ahead the block exists to remove.
Run that for D = 30 June 2015 and you get the index as it actually was in 2015, including the names that are gone. That is the universe your test should have used.
Note the two flags are separate for a reason. IsActiveNow says whether the company is in the index now; IsDelisted says whether the security still trades at all. A company can be out of the index and perfectly alive, or delisted and long gone, and those are different problems for your data pipeline.
Membership is only half the fix
Getting the names back is the easy half. Getting their prices back is where the work is.
A company that left the index because it failed also stopped being a listed ticker, and by default a symbol list returns only active symbols. /exchange-symbol-list/{exchangeCode} takes delisted=1 to include inactive tickers, which is how you recover the names. You still need their price history, and delisted history is exactly where coverage is thinnest and where the stale-and-missing problems from Reading the Market concentrate.
There is a further wrinkle: a ticker code can be reassigned. A three-letter code that meant one company in 2012 can mean a different one today, so a naive join on ticker will silently splice two companies into one series. The symbol-change history endpoint exists to answer that question, and the News, Signals and Discovery course covers it.
How big is the effect?
Large enough to change conclusions, and the honest answer to "how large" is that it depends entirely on the period, the universe and the strategy. Rather than quote somebody's figure, measure your own:
Run the identical test twice. Once on current members. Once on point-in-time members reconstructed from HistoricalTickerComponents. Report both numbers and the gap between them.
That gap is what your universe construction cost you — survivorship and look-ahead together, since rebuilding point-in-time membership fixes both at once — measured on your data, for your idea, over your period. It is the only version of the number that means anything, and producing it is the single most valuable habit in this entire course.
Nothing here evaluates any strategy or any index. It describes a measurement error and how to detect it.
Try it now
HistoricalTickerComponentsforGSPC.INDX, counted on 28 September 2026:
| Rows | Count |
|---|---|
| All | 822 |
EndDate populated |
319 |
IsActiveNow = 1 |
503 |
IsDelisted = 1 |
176 |
StartDate null |
145 |
Express the populated EndDate rows as a share of all rows. Those are the companies your current-members list would have hidden from you.
2. Seven rows of the same block, as the marketplace endpoint sent them that day:
Code |
Name |
StartDate |
EndDate |
|---|---|---|---|
| A | Agilent Technologies Inc | 2000-06-05 | null |
| AAL | American Airlines Group | 2015-03-23 | 2024-09-23 |
| AAP | Advance Auto Parts Inc | 2015-07-09 | 2023-08-25 |
| SIVB | SVB Financial Group | 2018-03-19 | 2023-03-15 |
| ABNB | Airbnb Inc | 2023-09-18 | null |
| AMTM | Amentum Holdings Inc. | 2024-09-30 | 2024-12-23 |
| LEH | Lehman Brothers Holdings Inc | null | 2008-09-16 |
Apply the point-in-time filter for D = 28 September 2021, then for D = 1 September 2008. Write down what the rule does with Lehman, whose StartDate is null, and the change your code needs because of it. Run over all 822 rows on 28 September 2026, the rule found 490 members on 28 September 2021 and 501 on 25 September 2026, with 67 names leaving and 78 joining in between; say which way the 145 null start dates bias the first count.
3. The graveyard, counted for the US on 28 September 2026:
| Call | Rows |
|---|---|
/exchange-symbol-list/US |
51,003 |
/exchange-symbol-list/US?delisted=1 |
60,303 |
The two lists share no code: the second holds only what has gone. Write the ratio of the second to the first, and the share of all US tickers, live and dead together, that a symbol list without delisted=1 never shows you.