HedgeFriend
← All posts
Data8 min readHedgeFriend

CUSIP, CIK, ticker, LEI: four identifiers, none of them primary keys

Every financial dataset is keyed on something that changes. Here is what each identifier actually names, how each one breaks, and what we key on instead.

Ask which identifier to join financial data on and you will get four answers. The ticker, because it is what everyone types. The CIK, because it is what the SEC uses. The CUSIP, because it is what holdings data uses. The LEI, because it is what regulators keep asking for. All four are useful. None of them is a primary key, because each one names a different thing, and each one changes or collides on a schedule you do not control.

This post is the field guide we wish we had when we started: what each identifier names, the specific ways it breaks, and the numbers from our own symbol table that show how often that happens.

The short version

  • Ticker names a listing, right now. It is display data.
  • CIK names an SEC filer. That is an entity, not a security, and sometimes not a company at all.
  • CUSIP names a security issue. It is the closest thing to a security ID, and it is licensed.
  • LEI names a legal entity, globally. It is clean, and almost absent from the data most people actually use.

Each one answers a different question: which listing, which filer, which security, which legal person. A join only works if both sides were asking the same question. Most identity bugs come from joining on one of these while assuming it means one of the others.

Ticker: a name tag, not an identity

A ticker is the string an exchange lets a listing use today. It is the right thing to show a human and the wrong thing to store a foreign key on, for three reasons.

Companies rename. Square became Block and SQ became XYZ. Bank of New York Mellon became BNY and gave up BK. Marsh & McLennan dropped MMC for MRSH this January. Data keyed on the ticker string breaks at each of these: the history sits under a symbol nobody queries anymore, and the new symbol starts with no history at all.

Tickers get reused. A ticker freed by a rename or a merger goes back into the pool. PARA meant Paramount for years. In our symbol table today, PARA is a different company, with a different CIK, while the post-merger Paramount trades as PSKY and files under a new CIK of its own. Any dataset that joined on the string PARA across 2025 has quietly spliced two companies together.

And nobody writes them the same way. Berkshire's class B shares are BRK-B in some places, BRK.B in others, BRK/B in a third, and BRKB in the SEC's own fails-to-deliver file. A dataset that drops the separator cannot reliably be split back apart: BRKB looks fine, but there is no general rule for whether a trailing letter is a share class or just part of the ticker.

Then there are the suffixes that mean something. A fifth letter on an OTC ticker carries meaning: F for a foreign ordinary, Y for an ADR, Q for an issuer in bankruptcy, D for a temporary symbol after a reverse split. BioXcel trading as BTAIQ next to BTAI, or Athena Gold as AHNRD next to AHNRF, is the same security on two strings during a transition. Treat them as separate tickers and you double-count. Strip the suffix and you lose the one fact the suffix was there to tell you.

CIK: an entity, not a security

The SEC's Central Index Key is a ten-digit, zero-padded number assigned to anyone who files on EDGAR. It is stable, it is free, and it is the right key for anything that comes out of a filing. It is also not a security identifier, and treating it like one causes most SEC join bugs.

One CIK, many securities. Of the 11,418 active symbols we carry, 4,762 share their CIK with at least one other symbol, spread across 1,788 CIKs. Alphabet has six symbols under one CIK. Bank of America has seventeen, nearly all preferred series. Public Storage has seventeen. Bank of Montreal has forty-five, almost all of them leveraged exchange-traded notes. Any filing-level fact (a 10-K, a late-filing notice, an 8-K) applies to every one of them equally.

We learned this the expensive way. Our filings table originally treated an SEC accession number as globally unique. Parallel workers raced to insert the same Alphabet 10-Q for GOOG and GOOGL, one won, and the other silently dropped. GOOGL's filings looked permanently stale, which froze its fundamentals, and the first sign was a freshness test failing weeks later. The fix was to make uniqueness per symbol and per accession. The general lesson: an SEC accession belongs to a filer, and a filer can have many securities.

It is not always one-to-one in the other direction either. Spin-offs carry the parent's pre-spin filing history: two companies with different CIKs can both legitimately list the same historical accession. And a CIK names an entity, not a business. Mergers often create a new registrant, as Paramount's did, so a company's filing history can live under two CIKs. Insiders and institutions get CIKs too: every Form 4 reporting owner and every 13F filer has one. A column of CIKs is not a column of companies.

Key filing data on the CIK and apply it to every security underneath. Never key security data on the CIK and hope there is only one.

CUSIP: the real security ID, with strings attached

A CUSIP is nine characters: six for the issuer, two for the specific issue, one check digit. It identifies a particular security, so GOOG and GOOGL get different CUSIPs where they share a CIK. That makes it the natural key for holdings data, and 13F filings identify positions by CUSIP. Each row has a free-text issuer name and an optional FIGI column, but no ticker and no CIK.

Three things make it less clean than it looks.

  1. 1.It changes on corporate actions. Reverse splits, reorganizations, and some name changes produce a new CUSIP for what an investor would consider the same stock. A CUSIP is stable across ticker renames far more often than the reverse, but not always.
  2. 2.It does not map to one ticker. In our own table, 162 CUSIPs map to more than one symbol row. Nearly all of them are the same security under two strings: an old and new ticker around a rename, or a foreign ordinary next to its D-suffixed post-split twin. That is the ticker problem showing up from the other side.
  3. 3.It is licensed. CUSIPs are issued and licensed by a private agency on behalf of the industry. Filings that carry them are public, but a ticker-to-CUSIP crosswalk as a product is the thing being licensed. That is why there is no good free one, and why we do not publish one either.

The licensing creates a quieter failure that matters more in practice. If your holdings pipeline maps 13F rows to your universe by CUSIP and a symbol has no CUSIP on file, that symbol gets zero institutional holders. That looks exactly like a stock nobody owns. Nothing errors. We found this in our own coverage audit: earlier this month, institutional holdings covered 13.5% of our tracked universe, because most tracked symbols had no CUSIP to match against. After we backfilled CUSIPs, that figure is 533 of 534.

The other side of the same join: of the roughly 788,000 raw 13F positions we hold since 2020, 66.1% map to a security in our symbol table. By reported value, it is 96.0%. The unmatched third of rows is the long tail: bonds, warrants, preferreds, foreign listings, and issuers too small to carry. If you compute any per-security 13F statistic, decide up front whether you want rows or dollars. The two give very different pictures of coverage.

LEI: correct, and mostly missing

The Legal Entity Identifier is a twenty-character code under ISO 17442, built after 2008 so regulators could tell which legal entity was on the other side of a trade. It is global, free to look up, and actually designed as an identifier, which puts it ahead of the other three.

Its problem is reach. An LEI names a legal entity, not a security, so it answers the same question as a CIK, but globally. It shows up in derivatives reporting, fund holdings disclosures, and European regulatory data. It does not show up in Form 4s, 13Fs, 8-Ks, or most of what equity researchers actually query. A listed group usually has many LEIs, one per subsidiary, and the listed parent is just one of them. We do not carry LEIs today, because nothing we ingest joins on them. If you work with swaps or cross-border fund data you will need them. For US equity signals, a CIK does the same job and is everywhere.

What we key on instead

Internally every row in every table hangs off an integer `symbol_id` that we assign and never reuse. The ticker is a mutable attribute of that row. The CIK and CUSIP are crosswalk columns used to attach outside data to it. Concretely:

  • A rename updates one column. The symbol keeps its id, so its history stays attached. The old ticker goes into a history table with the date it stopped being valid.
  • Filing-level feeds match on CIK and fan out to every symbol under it. A late-filing notice from Alphabet shows up on GOOG and GOOGL alike.
  • Holdings match on CUSIP. Rows that do not map are kept raw rather than dropped, so they can still be queried by CUSIP directly.
  • API paths take the ticker as it is today, because that is what people type. Ask for SQ and you get a 404, not Block. Ask for XYZ and you get Block's full history, including everything filed while it traded as SQ.

The last point is a deliberate tradeoff. Resolving old tickers silently would be friendlier until the first time a recycled ticker returned the wrong company's history with no warning. A 404 on a stale ticker is annoying. A confident answer about the wrong company is much worse.

Querying it

The raw 13F endpoint shows both sides of the CUSIP join. Pass a ticker and it resolves to the security's CUSIP for you. Pass a CUSIP directly and you also get positions in securities we do not carry, with `ticker` returned as null:

curl
bash
# By ticker: resolved to the CUSIP of that specific share class.
curl -H "Authorization: Bearer $HEDGEFRIEND_KEY" \
  "https://api.hedgefriend.dev/v1/sec/13f/holdings?symbol=GOOGL&report_date=2026-06-30"

# By CUSIP: works for issuers outside our symbol table too.
curl -H "Authorization: Bearer $HEDGEFRIEND_KEY" \
  "https://api.hedgefriend.dev/v1/sec/13f/holdings?cusip=67066G104&report_date=2026-06-30"

If you are joining our data to a source with different conventions, normalize class-share separators before anything else. It is the most common reason a join quietly loses a few dozen rows:

normalize_ticker.py
python
import re

def normalize(ticker: str) -> str:
    """Map BRK.B / BRK/B / BRK B to the dash form our API uses."""
    return re.sub(r"[./ ]", "-", ticker.strip().upper())

assert normalize("brk.b") == "BRK-B"
assert normalize("BF/A") == "BF-A"

# Deliberately NOT handled: separator-less forms like "BRKB".
# There is no rule that tells you whether a trailing letter is a share
# class or just part of the ticker. Resolve those against a list of
# known tickers, never with a regex.

The rule of thumb

Pick the identifier that names the thing your data is about. Filings are about filers, so use the CIK. Positions are about securities, so use the CUSIP. Counterparties are about legal entities, so use the LEI. Show humans the ticker. Then assign your own key and treat all four as attributes that point into it, because every one of them will eventually change underneath you, and you want that to be a one-row update rather than a migration.

None of this is glamorous. But most odd-looking results in financial data come from identity problems, not markets: a stock with zero institutional holders, a company whose filings stopped in April, a backtest that held Paramount and Banzai as one position. Rule out identity first and you will rarely be fooled for long.

Try it on your own data

The free tier covers 500 requests a day — enough to reproduce anything in this post.

Get a free key