Why Ticker Symbols Break Your Dataset
Ticker symbols break datasets in four ways: renames, reuse, share class punctuation, and mergers. See why each corrupts a join, and what to key on.
Ticker symbols break datasets in ways that never raise an error. A ticker is a display label an exchange assigns and can reassign, not a permanent name for a company, and every join written on that label inherits the instability. Below are the four ways a ticker corrupts a market-data table, each one visible in the data, plus the identifier to key on instead.
The four ways a ticker symbol breaks a dataset
- A rename. The company keeps trading, the symbol changes, and one continuous history splits across two strings.
- A reuse. A symbol goes dark on a delisting, sits idle, then gets assigned to an unrelated company.
- A punctuation disagreement. Share-class symbols carry a separator, and vendors and exchanges pick different ones.
- A disappearance. An acquisition closes out the symbol and nothing succeeds it.
A ticker join encodes one assumption: the same string means the same company on both sides, on every date in the sample. Each failure mode breaks that assumption in a different direction, and no database can detect it. The row either matches something it should not, or it drops out of the result. Both outcomes look like a clean run.
What happens when a company changes its ticker symbol?
A rename moves a live company from one symbol to another on a chosen date. Nothing about the business or the share count changes. The tape starts printing under a new string and the old one stops mid-history, with no marker in the price file. Facebook to Meta Platforms in June 2022 is the canonical case, and a session count makes it visible.
The exact SQL behind every number
SELECT
toString(toYear(date)) AS year,
countIf(ticker = 'FB') AS fb_sessions,
countIf(ticker = 'META') AS meta_sessions
FROM global_markets.stocks_daily_aggs
WHERE ticker IN ('FB', 'META')
AND date >= '2012-01-01'
GROUP BY year
ORDER BY yearThe panel counts trading sessions per calendar year under each symbol. In 2012, FB carries 155 sessions and META carries 0. In 2026, META carries 152 sessions and the string FB carries 151: both columns print in the latest year on the panel. Across the 15 years shown, no single column is one company's continuous history, and the FB column is not even guaranteed to be one company. A string that stops carrying a business can later be issued to another one, which is the second failure mode landing on top of the first.
A five-year price pull for META returns whatever the vendor filed under META. Some vendors restate the whole history backward under the current symbol; others leave the older bars under FB and hand back a shorter series. Both conventions are defensible, and neither is labeled in the output. The same ambiguity runs through split-adjusted price history, where the value in a cell depends on when the series was built.
Can two different companies share the same ticker symbol?
Yes, in sequence. Exchanges recycle symbols. When a listing ends the string returns to the pool, and it can be issued to an unrelated company months or years later. Nothing in a daily-bar file marks the handover.
The exact SQL behind every number
SELECT
toString(toYear(i.listing_day)) AS listing_year,
count() AS listings,
countIf(f.first_print < subtractDays(i.listing_day, 90)) AS symbols_with_earlier_history
FROM
(
SELECT
ticker,
min(toDate(listing_date)) AS listing_day
FROM global_markets.stocks_ipos
WHERE listing_date >= '2016-01-01'
AND listing_date < today()
GROUP BY ticker
) AS i
INNER JOIN
(
SELECT
ticker,
min(date) AS first_print
FROM global_markets.stocks_daily_aggs
GROUP BY ticker
) AS f ON f.ticker = i.ticker
GROUP BY listing_year
ORDER BY listing_yearThe panel takes every symbol that began a new listing since 2016 and asks one question of the price archive: had that same string already printed daily bars more than ninety days before the listing date? In 2016, 32 of 126 new listings landed on a symbol that already carried older bars. In 2026 the count is 31 of 198. The ninety-day buffer keeps when-issued and pre-listing prints out of the tally.
A backtest that loads the full file for one of those symbols and assumes one company sits behind it will splice the two together. The equity curve comes out continuous and plausible. It is two unrelated businesses laid end to end, the mirror image of survivorship bias in stock data: instead of a name quietly leaving the sample, an unrelated name quietly joins it.
Why do share-class tickers differ between vendors?
A company with two classes of stock needs two symbols, and the exchange separates the class letter from the root with a punctuation mark. Which mark depends on who is writing it down: a period, a hyphen, a slash, or nothing at all. Exchange convention and vendor normalization rarely agree, and neither is wrong.
Ten candidate spellings for two dual-class names go into the panel. 4 of them return any history at all. The first, BF.A, carries 4174 daily bars in the window queried, starting Jan 4, 2010.
The spellings that come back empty are not vendor errors. They are the other conventions, and a join written against one of them returns zero rows without complaint: no unmatched-key warning, just a quietly smaller result. Formatted identifier strings are a hazard of their own, which is what makes how to read an options symbol a skill in its own right.
What happens to a ticker after a merger or acquisition?
In a cash acquisition the target's shares are cancelled at the deal price and the symbol stops printing on the closing date. There is no successor row and no forwarding address. The last bar is simply the last bar.
The exact SQL behind every number
SELECT
ticker,
formatDateTime(max(date), '%b %e, %Y') AS last_print_label,
dateDiff('day', max(date), today()) AS days_since_last_print
FROM global_markets.stocks_daily_aggs
WHERE ticker IN ('ABMD', 'TWTR', 'ATVI', 'VMW', 'SGEN', 'AAPL')
AND date >= '2014-01-01'
GROUP BY ticker
ORDER BY days_since_last_print DESCEvery symbol on this panel except the bottom one was acquired and cancelled. The bottom one is a live control. TWTR sits at the top, its final daily bar dated Oct 27, 2022, 1383 days before this page was generated. AAPL is 0 days stale, which is ordinary ingest lag on a symbol that still trades. All 6 rows come from one table and one query.
Join a price history like this against a fundamentals table that carries only live companies and the cancelled names drop out. The row count falls. The average across the period changes. Nothing in the log mentions it. A sample that silently stops matching the sample that existed belongs to the same family of defect as look-ahead bias in backtesting, arriving through the join rather than through the clock.
The join that returns the wrong number
Standard-library Python makes the failure concrete. Two small tables straddle a rename: a price history that keeps the symbol printing on each date, and a fundamentals file restated under the current symbol, which is how most vendors ship it. The values are invented so the arithmetic stays readable.
#!/usr/bin/env python3
"""One rename, two joins. The numbers are invented. The failure is not."""
# A price file keeps the symbol that was printing on the day.
PRICES = [
("OLD", "2021-06-30", 100.0),
("OLD", "2021-12-31", 110.0),
("NEW", "2023-06-30", 240.0),
("NEW", "2023-12-29", 260.0),
]
# A fundamentals file restates every period under today's symbol.
FUNDAMENTALS = [
("NEW", "2021-06-30", 20.0),
("NEW", "2021-12-31", 22.0),
("NEW", "2023-06-30", 24.0),
("NEW", "2023-12-29", 26.0),
]
# permanent id -> every symbol it has worn, and the window each was valid for
SYMBOL_HISTORY = {
"ID-0001": [
("OLD", "2012-05-18", "2022-06-08"),
("NEW", "2022-06-09", "9999-12-31"),
],
}
def resolve_asof(ticker, day):
"""Permanent id for a symbol as it stood on a given date."""
for permanent_id, spans in SYMBOL_HISTORY.items():
for symbol, start, end in spans:
if symbol == ticker and start <= day <= end:
return permanent_id
raise KeyError(f"no permanent id for {ticker} on {day}")
def resolve_current(ticker):
"""Permanent id for a symbol as a restated file stamps it today."""
for permanent_id, spans in SYMBOL_HISTORY.items():
if spans[-1][0] == ticker:
return permanent_id
raise KeyError(f"no permanent id for {ticker}")
def by_ticker():
book = {(t, d): rev for t, d, rev in FUNDAMENTALS}
return [px / book[(t, d)] for t, d, px in PRICES if (t, d) in book]
def by_identity():
book = {(resolve_current(t), d): rev for t, d, rev in FUNDAMENTALS}
return [px / book[(resolve_asof(t, d), d)] for t, d, px in PRICES]
for label, ratios in (("ticker", by_ticker()), ("identity", by_identity())):
print(f"keyed on {label:9s} {len(ratios)} rows, "
f"mean price to revenue {sum(ratios) / len(ratios):.2f}")
The ticker join returns two rows and a mean price to revenue of 10.00. The identity join returns four rows and 7.50. Both runs exit zero and both print a number that looks like an answer. The ticker version dropped the pre-rename half of the sample without mentioning it, and the figure it reported is the correct aggregate of a different sample.
The correction sits in the signature. resolve_asof takes a symbol and a date; resolve_current takes a symbol and knows it means the symbol as stamped today. A symbol on its own is not a key. A symbol plus a date is.
What should you key a market-data table on?
Four families of permanent identifier are in common use. Choosing among them is mostly a question of what you are willing to pay and what you are willing to be locked into.
- CUSIP. A nine-character code for North American issues, administered under license. The first six characters identify the issuer and the next two the specific issue, so one company's common stock and its bonds each get their own. Redistribution is contractual. It is reissued on some corporate actions, which makes it permanent for an issue rather than for a company.
- ISIN. A twelve-character international wrapper: a country prefix, the national number inside it, and a check digit. For a US security that national number is the CUSIP, and the licensing questions travel with it.
- FIGI. The Financial Instrument Global Identifier, twelve characters, published as an open standard and free to use and to redistribute. It is deliberately granular: one FIGI per exchange listing, a composite FIGI grouping those within a country, and a share-class FIGI above that. Picking the right level for your table is your job.
- A vendor composite identifier. Most providers maintain an internal permanent id and the symbol-history table that goes with it. It costs nothing past the subscription and it is usually the best-maintained mapping you have. It does not travel: a second provider's id space will not agree with the first.
Whichever you adopt, the artifact you actually need is the symbol-history table: permanent id, symbol, valid from, valid to, one row per span. That is SYMBOL_HISTORY in the script above. A raw bar file will not include it, and if you are assembling a stack from a free stock market data API, the mapping is the piece you will build by hand.
Store the ticker for display, never for identity
That is the whole rule, and it holds against all four failure modes above. A symbol belongs in a column you render on a screen and in the search box a person types into. It does not belong in a primary key, a foreign key, a join predicate, or a file name that something downstream parses. Where identity matters, carry the permanent identifier next to the date and resolve the symbol at the last possible moment: render time.
FAQ
Why did my join lose rows after a company changed its ticker symbol?
The price history keeps the symbol that was printing on each date, and the other table was restated under the current symbol. The two strings stop matching on the older dates, and those rows fall out of an inner join with no error. Resolving both sides to a permanent identifier before joining brings them back.
Can two companies have the same ticker symbol at the same time?
Not within one listing venue's namespace. Symbols are unique while a listing is live. They get recycled after a delisting, which is how a single string ends up with two unrelated companies behind it in a long price archive.
Is BRK.A or BRK-A the correct ticker symbol?
Both, and neither. The class separator is a formatting convention rather than part of the security's identity, and sources normalize it differently. Query the spellings your own source actually stores before hardcoding one.
Do I need a paid identifier to key a market-data table?
No. An open identifier such as FIGI is free to use and free to redistribute, and a provider's internal permanent id ships with the data you already have. Paid identifiers buy coverage and cross-referencing, not the basic ability to key a table correctly.
What happens to price history after a company is acquired?
The symbol stops printing on the closing date and the historical bars stay in the archive under that symbol. No successor record links the target to the acquirer. That link is a mapping you maintain yourself or source from a corporate-actions feed.
Every panel here ships with the SQL that produced it; open one to see exactly which rows were counted. To pull a symbol's own history, the first bar and the last bar on file, ask for it in plain English on the Strasmore terminal.