休市提醒日股将于 2026-11-03 休市(文化日),距今天还有 5 天。全部公告
文章

跨市场代码体系映射:ticker、ISIN 与 CUSIP 的分工与陷阱

ticker 不是标识符,只是交易所内部的简称。本文讲清四套代码体系的分工,说明为什么用 ticker 当主键必然出问题,给出复合键加映射表的主键设计,以及代码规范化与 ISIN 校验的 Python 实现。

「用 ticker 当主键」是行情系统里最常见的一个决定,也是后期最难改的一个。原因是:ticker 不是标识符,它只是一个交易所内部的简称。 同一个 ticker 在不同市场指不同标的,同一个标的在不同时期有不同 ticker。

这篇文章讲清四套标识体系的分工,以及为什么你的数据模型需要它们。

一个标的有四套「名字」

体系示例特性适合做什么
Ticker / 交易代码AAPL、BTCUSDT、EURUSD交易所内部使用,简短、可能重复、可能变更下单、请求行情
ISINUS037833100512 位,全球唯一,覆盖股票与债券跨市场唯一标识
CUSIP0378331009 位,北美市场为主北美证券识别
交易所 + 代码NASDAQ:AAPL带交易所前缀,消除跨市场歧义行情请求、图表引用

关键区别在于:ISIN 是标识符(identifier),ticker 是符号(symbol)。 标识符在整个生命周期内不变,符号会变。

为什么 ticker 不能当主键

三个具体原因,每一个都会在生产环境造成数据错乱:

原因一:同名不同标的

同一个 ticker 在不同交易所指向完全不同的公司。00700 在香港市场是腾讯,在其他市场可能指向完全无关的标的。如果你的数据库主键是裸 ticker,跨市场数据会直接串在一起。

原因二:代码会变更

公司改名、重组、并购、更换交易所,都会导致 ticker 变化。历史上使用 ticker 做主键的系统,在遇到代码变更时要么丢失历史连续性,要么需要大规模数据迁移。

原因三:同一标的多个代码

同一家公司在不同市场上市,会有多个 ticker(例如同时在美股和港股上市)。这些 ticker 指向同一经济实体,但用 ticker 做主键会被视为不同标的。

映射过程中的三个陷阱

  1. 资产类别前缀不统一:加密的 BTCUSDT、外汇的 EURUSD、贵金属的 XAUUSD 命名规则各不相同。跨类别处理时不能假设「都是 3–5 个大写字母」。
  2. 大小写与分隔符敏感:BTCUSDT 与 btcusdt、BTC-USDT 与 BTCUSDT 在部分系统里是不同的键。接入前必须明确规范化规则。
  3. 指数与期货没有 ISIN:指数、连续期货、部分衍生品没有 ISIN 或 CUSIP。这意味着你的主键设计不能只依赖 ISIN。

第三个陷阱最容易被忽略。很多团队设计主键时假设「一切都有 ISIN」,直到接入指数或期货时才发现模型不成立。

正确的主键设计

务实的主键方案是复合键 + 映射表:

  1. 内部主键用自增 ID 或 UUID:与外部代码体系解耦,外部变更不影响内部引用。
  2. 存市场 + 代码的组合:(market, code) 作为业务唯一键,解决跨市场同名问题。
  3. ISIN 作为可选的全局标识:有则存,用于跨市场去重;没有(指数、期货)则留空。
  4. 维护映射表:记录每个内部 ID 对应的各体系代码,以及代码的历史变更。
  5. 代码变更记录生效时间:(internal_id, system, code, valid_from, valid_to) 这种带时间维度的结构,才能正确处理历史数据。

这套设计多花一点前期成本,但避免了后期最痛苦的一类数据迁移。

Python 实现:代码规范化与映射

Python
import re
from dataclasses import dataclass, field
from datetime import date

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


@dataclass
class Instrument:
    """内部主键与外部代码解耦。"""
    internal_id: int
    market: str
    category: str                     # equity / fx / crypto / futures / index
    code: str                         # 当前使用的交易代码
    isin: str | None = None
    aliases: list[str] = field(default_factory=list)   # 历史代码或别名


def normalize_code(code: str, category: str) -> str:
    """统一规范化:去空格、大写。不要擅自删除分隔符。"""
    code = code.strip().upper()
    # 加密与外汇通常无分隔符;股票代码可能含点号(如 000001.SZ),必须保留
    if category in ("crypto", "fx") and "-" in code:
        code = code.replace("-", "")
    return code


def validate_isin(isin: str) -> bool:
    """ISIN 校验:12 位 + Luhn 校验位。"""
    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]:
    """业务唯一键:市场 + 代码,解决跨市场同名。"""
    return (inst.market, inst.code)


# 同一 ticker 在不同市场指向不同标的 —— 复合键才能区分
a = Instrument(1, "hk", "equity", "00700")
b = Instrument(2, "other", "equity", "00700")
print("业务键不同:", business_key(a) != business_key(b))

print("ISIN 校验 US0378331005:", validate_isin("US0378331005"))
print("ISIN 校验 US0378331004:", validate_isin("US0378331004"))
print("规范化 btc-usdt ->", normalize_code("btc-usdt", "crypto"))
print("规范化 000001.SZ 保留点号 ->", normalize_code("000001.sz", "equity"))

几个要点:业务键用 (market, code) 而不是裸 code;ISIN 做 Luhn 校验而不是只验格式;规范化时不要粗暴删除分隔符——股票代码里的点号(如 000001.SZ)是有语义的,删掉就变成另一个市场的代码了。

行情请求中的代码处理

行情接口的多市场查询使用 codes 参数,单次最多 50 个,接受产品代码或图表符号。实践中有两点需要注意:

  • 请求前先规范化:把用户输入或上游数据统一成接口期望的格式,避免因大小写或分隔符导致 INVALID_SYMBOL。
  • 用返回的 market 与 category 字段做校验:响应里带有市场与类别字段,可以用来确认你请求的代码被解析成了正确的标的,而不是同名的另一个市场的标的。

第二点尤其重要:不要假设代码一定被正确解析。 收到响应后校验 market 字段是否符合预期,能在早期发现映射错误。

常见问题

可以直接用 ticker 做数据库主键吗?

不建议。ticker 会变更、跨市场会重复、同一标的可能有多个。用内部 ID 做主键,把 ticker 与 ISIN 作为带时间维度的外部映射存储。

没有 ISIN 的标的怎么办?

指数、连续期货、部分衍生品没有 ISIN 或 CUSIP。所以主键设计不能只依赖 ISIN——用 (market, code) 作为业务键,ISIN 作为可选字段。

怎么处理代码变更(改名、换交易所)?

用带生效时间的历史记录表:(internal_id, system, code, valid_from, valid_to)。这样历史数据可以按当时有效的代码正确关联,而不需要重写历史。

规范化代码时应该删除分隔符吗?

要按资产类别区别对待。加密与外汇通常无分隔符(BTCUSDT),可以安全去掉连字符;但股票代码里的点号有语义(000001.SZ 与 000001 是不同市场的标的),必须保留。

怎么验证代码被正确解析了?

校验响应中的 market 与 category 字段。接口会返回市场分类信息,用它可以确认请求的代码对应的是预期标的,而不是同名的另一个市场的标的。

下一步

把内部主键与外部代码解耦,是行情系统里回报最高的一次数据建模决定。代码体系的对照说明见 代码规则指南,ISIN 与 CUSIP 的转换可用 ISIN 转换工具;多市场请求的字段定义见 多市场行情接口文档;如果涉及多个市场的代码差异,可参考 美股行情 与 港股行情 页面上的代码示例。