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