「用 ticker 当主键」是行情系统里最常见的一个决定,也是后期最难改的一个。原因是:ticker 不是标识符,它只是一个交易所内部的简称。 同一个 ticker 在不同市场指不同标的,同一个标的在不同时期有不同 ticker。
这篇文章讲清四套标识体系的分工,以及为什么你的数据模型需要它们。
一个标的有四套「名字」
| 体系 | 示例 | 特性 | 适合做什么 |
|---|---|---|---|
| Ticker / 交易代码 | AAPL、BTCUSDT、EURUSD | 交易所内部使用,简短、可能重复、可能变更 | 下单、请求行情 |
| ISIN | US0378331005 | 12 位,全球唯一,覆盖股票与债券 | 跨市场唯一标识 |
| CUSIP | 037833100 | 9 位,北美市场为主 | 北美证券识别 |
| 交易所 + 代码 | NASDAQ:AAPL | 带交易所前缀,消除跨市场歧义 | 行情请求、图表引用 |
关键区别在于:ISIN 是标识符(identifier),ticker 是符号(symbol)。 标识符在整个生命周期内不变,符号会变。
为什么 ticker 不能当主键
三个具体原因,每一个都会在生产环境造成数据错乱:
原因一:同名不同标的
同一个 ticker 在不同交易所指向完全不同的公司。00700 在香港市场是腾讯,在其他市场可能指向完全无关的标的。如果你的数据库主键是裸 ticker,跨市场数据会直接串在一起。
原因二:代码会变更
公司改名、重组、并购、更换交易所,都会导致 ticker 变化。历史上使用 ticker 做主键的系统,在遇到代码变更时要么丢失历史连续性,要么需要大规模数据迁移。
原因三:同一标的多个代码
同一家公司在不同市场上市,会有多个 ticker(例如同时在美股和港股上市)。这些 ticker 指向同一经济实体,但用 ticker 做主键会被视为不同标的。
映射过程中的三个陷阱
- 资产类别前缀不统一:加密的
BTCUSDT、外汇的EURUSD、贵金属的XAUUSD命名规则各不相同。跨类别处理时不能假设「都是 3–5 个大写字母」。 - 大小写与分隔符敏感:
BTCUSDT与btcusdt、BTC-USDT与BTCUSDT在部分系统里是不同的键。接入前必须明确规范化规则。 - 指数与期货没有 ISIN:指数、连续期货、部分衍生品没有 ISIN 或 CUSIP。这意味着你的主键设计不能只依赖 ISIN。
第三个陷阱最容易被忽略。很多团队设计主键时假设「一切都有 ISIN」,直到接入指数或期货时才发现模型不成立。
正确的主键设计
务实的主键方案是复合键 + 映射表:
- 内部主键用自增 ID 或 UUID:与外部代码体系解耦,外部变更不影响内部引用。
- 存市场 + 代码的组合:
(market, code)作为业务唯一键,解决跨市场同名问题。 - ISIN 作为可选的全局标识:有则存,用于跨市场去重;没有(指数、期货)则留空。
- 维护映射表:记录每个内部 ID 对应的各体系代码,以及代码的历史变更。
- 代码变更记录生效时间:
(internal_id, system, code, valid_from, valid_to)这种带时间维度的结构,才能正确处理历史数据。
这套设计多花一点前期成本,但避免了后期最痛苦的一类数据迁移。
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 转换工具;多市场请求的字段定义见 多市场行情接口文档;如果涉及多个市场的代码差异,可参考 美股行情 与 港股行情 页面上的代码示例。