「用 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 轉換工具;多市場請求的字段定義見 多市場行情接口文檔;如果涉及多個市場的代碼差異,可參考 美股行情 與 港股行情 頁面上的代碼示例。