Quant500 Academy

4. The ticker lies: identifiers and recycling

Level 1: Where they come from and what they carry inside

You have spent three modules worrying about the numbers. Now the problem is the label that groups them. Almost everybody joins their sources on the ticker, because that is what comes out of the CSV and what you carry around in your head. It is the worst possible identifier, and it fails silently: it raises no exceptions, it simply hands you back the wrong company.

The ticker is a display label, not an identity

Three properties to burn into your memory:

The operational conclusion: a ticker only identifies something if it comes with a market and a date range.

Identifiers that do hold up

None of them is magic: each answers a different question, and the typical mistake is to join an entity identifier to a security identifier and lose the share classes along the way.

What happens to the historical series

When a company changes its code, most vendors rewrite the entire history under the new code. That is convenient and treacherous at the same time: the META series starts in 2012 even though that code did not exist until 2022. If another of your sources keeps the old series under the old code, joining on ticker leaves you with two pieces that do not overlap, and no warning whatsoever.

With mergers it is worse. The acquired company ceases to exist; its series ends. If the vendor "splices" the two histories together, you get a make-believe company, with a jump in price and in fundamentals that corresponds to no real shareholder. The first diagnostic is to count how many stable identifiers each code has carried:

import pandas as pd

fundam = pd.read_csv("fundamentales.csv", parse_dates=["fecha"])  # carries ticker and cik

colisiones = fundam.groupby("ticker")["cik"].nunique().sort_values(ascending=False)
print("Tickers con mas de una entidad detras:", (colisiones > 1).sum())
print(colisiones[colisiones > 1].head(15))

# And the other way round: entities that have used several tickers across the history.
mudanzas = fundam.groupby("cik")["ticker"].nunique()
print("Entidades que han cambiado de ticker:", (mudanzas > 1).sum())

If that first count does not come out at zero, every ticker-based join in your project is mixing companies together. The correct approach is to keep a map of effective date ranges and join on a stable identifier, not on a label:

precios = pd.read_csv("precios.csv", parse_dates=["fecha"])
mapa = pd.read_csv("mapa_tickers.csv", parse_dates=["desde", "hasta"])  # ticker, cik, desde, hasta

precios = precios.merge(mapa, on="ticker", how="left")

# Ranges still open have no end date: they must be closed before comparing.
precios["hasta"] = precios["hasta"].fillna(pd.Timestamp("2099-12-31"))

vigente = (precios["fecha"] >= precios["desde"]) & (precios["fecha"] <= precios["hasta"])
descartadas = (~vigente).sum()
precios = precios[vigente]

print("Filas fuera de la vigencia del ticker:", descartadas)
print("Filas sin identificador estable:", precios["cik"].isna().sum())

panel = precios.merge(fundam, on=["cik", "fecha"], how="inner")
print("Filas del panel final:", len(panel))

The discarded rows are exactly the ones that used to sneak in as another company's data. It is worth printing them and going through them one by one the first time: there is almost always a surprise in there.

Why this always fails silently

A ticker-based merge has no way of knowing that two rows with the same label are talking about different things. To pandas, "ABC" is "ABC". The result is not an exception but a perfectly normal-looking panel in which:

Hence the only discipline that works: never join two sources on ticker without first printing how many rows go in, how many come out and how many end up unmatched. If those three numbers do not square with what you expected, you have an identity problem, not a data problem.

A real case: buying the wrong Zoom

In early 2020, with lockdown pushing everyone onto video calls, plenty of people wanted to buy shares in Zoom Video Communications, which trades under the code ZM. There was also a different and minuscule company, Zoom Technologies, whose code was ZOOM. The intuitive code belonged to the wrong company.

The result was a preposterous rise in the small company over a few weeks, multiplying several times over with no news to justify it, until the SEC suspended trading in it on March 26, 2020, citing concern about the information circulating in the market and about investors possibly confusing it with a similarly named company.

There you have the whole module in a single case: two unrelated companies, near-identical codes, and a match made on the label instead of on the identity. If it happened to thousands of people staring at a screen, imagine how easy it is for it to happen to a pandas merge that never complains. Education, not financial advice.

To go deeper into this lesson

The rest of Quant500

What you are learning, applied to the index every day:

The model portfolio, weekly in your inbox

Every Saturday, its week in one minute: how it went, the best and the worst, and which insiders bought with their own money. Plus, the day the model is rebuilt, which stocks come in and which go out. A simulation, not advice. At most one email a week, no ads, no account; one-click unsubscribe. Made by one person: Pol Gracia.

A model portfolio with simulated results, not a personalized recommendation. Your email is used for this only: privacy.

Behind the Academy and the rest of Quant500 there is one person: Pol Gracia Serrahima.