Does Ticker Recycling Corrupt a Mean-Reversion Backtest? Entity-Resolved Z-Scores in Python
July 17, 2026
What's the question?
A ticker is a mailing address, not an identity. When a company delists or restructures, the exchange may hand its symbol to an entirely different business. The symbol persists. The company behind it does not.
A price series keyed on the symbol therefore splices two unrelated companies into one continuous history. A z-score mean-reversion signal — how far a price has strayed from its recent average, in units of its recent standard deviation — is defined entirely by a rolling lookback window. If that window holds a different company’s prices, the signal is meaningless. The hypothesis under test: a symbol handover injects false signals that are not marginal noise but confident, tradeable, and wrong.
The approach
The test case is the symbol AA. On 1 November 2016 Alcoa Inc renamed itself Arconic (today Howmet Aerospace), keeping its SEC filer identity, and spun off a new aluminium business, Alcoa Corporation, which took AA. SEC EDGAR confirms both legs to the day: CIK 0000004281 became "Arconic Inc." on 1 November 2016 and trades as HWM today; CIK 0001675149 shed the name "Alcoa Upstream Corp" on 31 October 2016. The dates are consecutive trading days, and the two filers sit in different sectors.
xfl.prices("AA") does not return a spliced series; it returns Alcoa Corporation only. The naive series below reconstructs what symbol-keyed data produces, without hand-picking dates: the API stamps each row with the ticker that entity traded under that day, so selecting on ticker == "AA" across both entities reproduces symbol-keyed data exactly.
- Read the entity history for AA with
resolve() - Pull both entities: Alcoa Corporation via
AA, the prior holder viaHWM - Build the naive series by symbol, the resolved series by entity
- Compute a 20-day rolling z-score on each; flag entries at |z| > 2
- Compare signal counts after the handover, then price the phantom trades
Adjusted close is used throughout, to avoid also stepping across Arconic’s October 2016 reverse split.
Code
import xfinlink as xfl
import pandas as pd
xfl.set_api_key("YOUR_API_KEY") # free at https://xfinlink.com/signup
START, END, WINDOW, THRESH = "2016-08-01", "2017-02-28", 20, 2.0
# Who has held the symbol "AA"?
for e in xfl.resolve("AA")["data"]["AA"]["entities"]:
print(e["name"], e["cik"], e["ticker_valid_from"], "->", e["ticker_valid_to"] or "present")
# prices("AA") returns ONLY the current holder (Alcoa Corp).
# The prior holder is reached through the symbol it trades under today: HWM.
old = xfl.prices("HWM", start=START, end=END, fields=["adj_close"])
new = xfl.prices("AA", start=START, end=END, fields=["adj_close"])
both = pd.concat([old, new], ignore_index=True)
# NAIVE: select on the SYMBOL -> whatever company held "AA" that day.
naive = both[both["ticker"] == "AA"].sort_values("date").reset_index(drop=True)
# RESOLVED: select on the ENTITY -> one company, no splice.
resolved = both[both["entity_id"] == new["entity_id"].iloc[0]].sort_values("date").reset_index(drop=True)
def zscore(d):
d = d.copy()
m = d["adj_close"].rolling(WINDOW).mean()
s = d["adj_close"].rolling(WINDOW).std()
d["z"] = (d["adj_close"] - m) / s
return d
naive, resolved = zscore(naive), zscore(resolved)
win = naive[naive["date"] >= "2016-11-01"].head(WINDOW)
winr = resolved[resolved["date"] >= "2016-11-01"].head(WINDOW)
sigs = win[win["z"].abs() > THRESH]
print("naive :", win["z"].notna().sum(), "scored,", len(sigs), "signals, max|z| =", round(win["z"].abs().max(), 2))
print("resolved:", winr["z"].notna().sum(), "scored,", int((winr["z"].abs() > THRESH).sum()), "signals")
Full script with formatting and visualisation: ticker-recycling-mean-reversion-backtest-python.py
Output
==========================================================================
ENTITY HISTORY FOR SYMBOL 'AA'
==========================================================================
HOWMET AEROSPACE INC CIK 0000004281 1962-07-02 -> 2016-10-31 (Industrials)
ALCOA CORP CIK 0001675149 2016-11-01 -> present (Materials)
Naive symbol-keyed series: 146 rows, 2016-08-01 -> 2017-02-28
ALCOA CORP 2016-11-01 -> 2017-02-28 (81 days)
HOWMET AEROSPACE INC 2016-08-01 -> 2016-10-31 (65 days)
Entity-resolved series: 81 rows, 2016-11-01 -> 2017-02-28
==========================================================================
THE SPLICE (no gap: consecutive trading days)
==========================================================================
2016-10-31 AA = 21.54 HOWMET AEROSPACE INC
2016-11-01 AA = 23.00 ALCOA CORP
Implied one-day return: +6.78% -- earned by no shareholder of either company
==========================================================================
FIRST 20 TRADING DAYS AFTER SPLICE (2016-11-01 -> 2016-11-29)
==========================================================================
Naive : 20 days scored, 6 signals (|z|>2.0), max|z| = 2.58
Resolved : 1 days scored, 0 signals -- insufficient history to score the rest
Phantom trades (z > +2 => mean-reversion SHORT, 5-day hold):
2016-11-03 z=+2.06 short 24.15 -> cover 29.10 -20.50%
2016-11-04 z=+2.42 short 25.20 -> cover 29.30 -16.27%
2016-11-07 z=+2.12 short 25.08 -> cover 29.62 -18.10%
2016-11-08 z=+2.29 short 26.40 -> cover 30.21 -14.43%
2016-11-09 z=+2.58 short 28.72 -> cover 31.42 -9.40%
2016-11-10 z=+2.21 short 29.10 -> cover 31.85 -9.45%
6 phantom shorts | mean -14.69% | cumulative -88.15% | win rate 0%
Alcoa Corp over the same stretch: 23.00 -> 31.85 (+38.5%)
==========================================================================
OTHER S&P 500 SYMBOLS WITH AN EDGAR-CONFIRMED HANDOVER BETWEEN DISTINCT FILERS
==========================================================================
(entity_name is the filer's CURRENT legal name, not its name at the time)
GM:
General Motors Corporation (pre-2009 bankruptcy) CIK 0000040730 1962-07-02 -> 2009-06-01
General Motors Company CIK 0001467858 2010-11-18 -> present
DELL:
DELL INC CIK 0000826083 1988-06-22 -> 2013-10-29
DELL TECHNOLOGIES INC CIK 0001571996 2018-12-28 -> present
BG:
CALERES INC CIK 0000014707 1977-04-04 -> 1999-05-27
BUNGE GLOBAL S A CIK 0001996862 2001-08-02 -> present
What this tells us
The naive series scores all 20 days after the handover and fires 6 signals. The resolved series scores 1 and fires none, correctly reporting that Alcoa Corporation lacks 20 days of history. The naive series believes it has 20 days; nineteen belong to an aerospace company. Its rolling mean on 3 November sits near Arconic’s $20 level, while Alcoa Corporation opened at $23.00 and ran higher. The z-score reads the new company as stretched above an average that is not its own.
The damage is directional, not random. All 6 signals point the same way, and Alcoa Corporation rallied 38.5% over the following fortnight. Priced against real closes, the phantom shorts return a mean of -14.69% with a 0% win rate. A researcher would conclude the strategy fails on AA. It never traded AA. A data defect did.
The maximum |z| is 2.58 — not an outlier. A 10-sigma print would be caught by any sanity filter, whereas a cluster between 2.0 and 2.6 looks like a textbook setup, which is why this error survives review. The contamination is bounded: the two z-series agree to machine precision from 29 November, exactly 20 trading days after the handover, once the window flushes the last Arconic price. The handover left no other trace: no date gap, no jump, and the name said Alcoa on both sides.
So what?
Resolve identity before computing anything with a lookback. Any statistic with a memory — a rolling mean, a volatility estimate, a beta — inherits whatever the window contains, and a window is exactly where a splice hides. Two practices follow: key every series on a persistent entity identifier, and require the lookback window to sit entirely within one entity’s life, suppressing signals until it does. That second rule alone eliminates all 6 phantom trades here.
AA is not a special case. Three current S&P 500 symbols show the same structure, each confirmed the same way: distinct filer CIKs, a rename or exit matching the handover date, and the current holder checked against EDGAR’s ticker registry. GM passed from General Motors Corporation — Motors Liquidation Company after 2009 — to General Motors Company in 2010; DELL from Dell Inc, taken private in 2013, to Dell Technologies in 2018; BG from Brown Group, today Caleres, to Bunge in 2001. Three is a floor: EDGAR does not record historical ticker assignments, and delisted names — where recycling concentrates — are excluded.
Screening entity history costs one resolve() call. Attributing a data defect to a strategy costs considerably more.
Built with xfinlink — free financial data API for Python. pip install xfinlink
pip install xfinlink