Market holidayJapan will be closed on 2026-11-03 (Culture Day), 5 days from now.All announcements
Article

Cross-Market Symbol Mapping: How Tickers, ISINs, and CUSIPs Fit Together

A ticker is not an identifier — it is an exchange-local abbreviation. We explain how four code systems divide the work, why ticker primary keys inevitably break, a composite-key design with mapping tables, and Python for normalisation and ISIN validation.

"Use the ticker as the primary key" is one of the most common decisions in market data systems — and one of the hardest to undo. The reason: a ticker is not an identifier, it is an exchange-local abbreviation. The same ticker means different instruments in different markets, and the same instrument carries different tickers over time.

This article lays out the division of labour between four identification systems, and why your data model needs them.

Every instrument has four "names"

SystemExampleCharacteristicBest used for
Ticker / trading codeAAPL, BTCUSDT, EURUSDExchange-local; short, possibly duplicated, mutableOrder routing, quote requests
ISINUS037833100512 characters, globally unique, covers equities and bondsCross-market identity
CUSIP0378331009 characters, primarily North AmericaNorth American security identification
Exchange + codeNASDAQ:AAPLExchange prefix removes cross-market ambiguityQuote requests, chart references

The key distinction: an ISIN is an identifier; a ticker is a symbol. Identifiers persist for the life of the instrument. Symbols change.

Why a ticker cannot be the primary key

Three concrete reasons, each of which corrupts production data:

Reason one: the same ticker, different instruments

The same ticker points to entirely different companies on different exchanges. 00700 is Tencent in Hong Kong and could be something unrelated elsewhere. If your primary key is a bare ticker, cross-market data silently merges.

Reason two: codes change

Renames, restructurings, mergers, and exchange migrations all change tickers. Systems keyed on tickers either lose historical continuity when a code changes or require a large data migration.

Reason three: one instrument, many codes

A company listed in several markets carries multiple tickers — dual-listed in the US and Hong Kong, for instance. Those tickers refer to the same economic entity, but a ticker-keyed model treats them as separate instruments.

Three mapping traps

  1. Inconsistent naming across asset classes. Crypto uses BTCUSDT, FX uses EURUSD, metals use XAUUSD — each with its own convention. Never assume "3–5 uppercase letters" when handling multiple classes.
  2. Case and separator sensitivity. BTCUSDT versus btcusdt, or BTC-USDT versus BTCUSDT, are distinct keys in some systems. Pin down normalisation rules before integration.
  3. Indices and futures have no ISIN. Indices, continuous futures, and some derivatives carry neither ISIN nor CUSIP. Your primary key design therefore cannot depend on ISIN alone.

The third trap is the most overlooked. Many teams design keys assuming everything has an ISIN, then discover the model breaks when indices or futures arrive.

A workable primary key design

The pragmatic approach is a composite key plus mapping tables:

  1. Internal primary key as an auto-increment ID or UUID. Decoupled from external code systems, so external changes do not break internal references.
  2. Store market plus code. (market, code) as the business unique key resolves cross-market duplicates.
  3. ISIN as an optional global identifier. Store it when present for cross-market deduplication; leave null for indices and futures.
  4. Maintain mapping tables. Record each internal ID's codes across systems, along with historical changes.
  5. Timestamp code changes. A structure like (internal_id, system, code, valid_from, valid_to) is what lets you resolve historical data correctly.

This costs a little more up front and avoids the most painful class of migration later.

Python: normalisation and mapping

Python
import re
from dataclasses import dataclass, field

ISIN_RE = re.compile(r"^[A-Z]{2}[A-Z0-9]{9}[0-9]#34;)


@dataclass
class Instrument:
    """Internal identity decoupled from external codes."""
    internal_id: int
    market: str
    category: str                     # equity / fx / crypto / futures / index
    code: str                         # currently used trading code
    isin: str | None = None
    aliases: list[str] = field(default_factory=list)   # historical codes


def normalize_code(code: str, category: str) -> str:
    """Normalise: trim and uppercase. Do not strip separators blindly."""
    code = code.strip().upper()
    # Crypto and FX are typically unseparated; equity codes may contain a dot (000001.SZ) and must keep it
    if category in ("crypto", "fx") and "-" in code:
        code = code.replace("-", "")
    return code


def validate_isin(isin: str) -> bool:
    """ISIN validation: 12 characters plus a Luhn check digit."""
    if not ISIN_RE.match(isin):
        return False
    digits = "".join(str(int(c, 36)) for c in isin)
    total, double = 0, False
    for d in reversed(digits):
        n = int(d)
        if double:
            n *= 2
            if n > 9:
                n -= 9
        total += n
        double = not double
    return total % 10 == 0


def business_key(inst: Instrument) -> tuple[str, str]:
    """Business unique key: market + code resolves cross-market duplicates."""
    return (inst.market, inst.code)


# The same ticker in different markets is a different instrument — only a composite key separates them
a = Instrument(1, "hk", "equity", "00700")
b = Instrument(2, "other", "equity", "00700")
print("distinct business keys:", business_key(a) != business_key(b))

print("ISIN check US0378331005:", validate_isin("US0378331005"))
print("ISIN check US0378331004:", validate_isin("US0378331004"))
print("normalise btc-usdt ->", normalize_code("btc-usdt", "crypto"))
print("normalise 000001.sz keeps the dot ->", normalize_code("000001.sz", "equity"))

A few points stand out: the business key is (market, code), not a bare code; ISIN validation runs a Luhn check rather than just a format match; and normalisation must not strip separators blindly — the dot in an equity code like 000001.SZ is semantic, and removing it yields a code for a different market.

Handling codes in quote requests

The multi-market quote endpoint takes a codes parameter, up to 50 per call, accepting product codes or chart symbols. Two things matter in practice:

  • Normalise before requesting. Convert user input or upstream data into the format the API expects, so case or separator issues do not trigger INVALID_SYMBOL.
  • Validate using the returned market and category fields. Responses carry market and category metadata you can use to confirm the code resolved to the intended instrument rather than a same-named one in another market.

The second point matters more than it looks: never assume a code resolved correctly. Checking the market field against expectations surfaces mapping errors early.

FAQ

Can I just use the ticker as my database primary key?

Not advisable. Tickers change, repeat across markets, and multiply for dual-listed instruments. Use an internal ID as the key and store tickers and ISINs as external mappings with validity periods.

What about instruments with no ISIN?

Indices, continuous futures, and some derivatives have neither ISIN nor CUSIP. That is why the key design cannot rely on ISIN alone — use (market, code) as the business key with ISIN as an optional field.

How do I handle code changes from renames or exchange migrations?

Use a historical mapping table with validity periods: (internal_id, system, code, valid_from, valid_to). Historical data then resolves against the code valid at that time, with no need to rewrite history.

Should normalisation strip separators?

Treat it per asset class. Crypto and FX are usually unseparated (BTCUSDT), so hyphens can be removed safely; but a dot in an equity code is semantic — 000001.SZ and 000001 are instruments in different markets and the dot must stay.

How do I verify a code resolved correctly?

Check the market and category fields in the response. The API returns market classification metadata, which lets you confirm the requested code maps to the intended instrument rather than a same-named one elsewhere.

Next steps

Decoupling internal identity from external codes is the highest-return data modelling decision in a market data system. Code conventions are explained in the symbol code guide, and ISIN/CUSIP conversion is available in the ISIN converter. Request field definitions are in the multi-market quote docs. For code differences across markets, see the examples on the US stocks and Hong Kong stocks pages.