解答

t187ap03_L 與 mopsfin_t187ap03_O 是什麼?上市櫃公司基本資料端點

這兩個端點是同一份資料的兩個市場版本:t187ap03_L 提供上市公司基本資料,mopsfin_t187ap03_O 提供上櫃公司基本資料。內容包含公司全名、簡稱、產業別、統一編號、董事長、總經理、發言人、地址等。最大的差別是欄位命名——上市是中文,上櫃是英文。

這兩個端點是什麼

公司基本資料(company profile)是把代號對應到公司實體的主檔——做任何跨資料集分析前,你都需要它來對映代號、產業與公司名稱。上市版在 TWSE 網域、上櫃版在 TPEx 網域,2026-07-31 實測分別回傳約一千一百筆與約九百筆。

# 上市
curl "https://openapi.twse.com.tw/v1/opendata/t187ap03_L"

# 上櫃
curl "https://www.tpex.org.tw/openapi/v1/mopsfin_t187ap03_O"

欄位對照:同一份資料,兩套命名

這是使用這組端點最需要留意的地方。兩邊的欄位語意幾乎一致,但命名語言不同,直接把兩個 JSON 陣列接起來會得到兩組不相容的 key。以下是主要欄位的對照:

  • 公司代號 ↔ SecuritiesCompanyCode
  • 公司名稱 ↔ CompanyName
  • 公司簡稱 ↔ CompanyAbbreviation
  • 產業別 ↔ SecuritiesIndustryCode
  • 營利事業統一編號 ↔ UnifiedBusinessNo.(注意結尾的句點也是 key 的一部分)
  • 董事長 ↔ Chairman;總經理 ↔ GeneralManager;發言人 ↔ Spokesman
  • 出表日期 ↔ Date(皆為民國年字串)
import requests

listed = requests.get("https://openapi.twse.com.tw/v1/opendata/t187ap03_L", timeout=30).json()
otc = requests.get("https://www.tpex.org.tw/openapi/v1/mopsfin_t187ap03_O", timeout=30).json()

def normalize(row, listed_market):
    if listed_market:
        return {"symbol": row["公司代號"], "name": row["公司簡稱"], "industry": row["產業別"], "market": "TWSE"}
    return {"symbol": row["SecuritiesCompanyCode"], "name": row["CompanyAbbreviation"],
            "industry": row["SecuritiesIndustryCode"], "market": "TPEx"}

universe = [normalize(r, True) for r in listed] + [normalize(r, False) for r in otc]
print(len(universe))

它的限制

  • 只有現況,沒有沿革:端點給的是當下的公司主檔,不含歷史變更。公司改名、換產業分類、董事長異動都只會看到最新狀態,回測時無法還原「當時」的樣貌。
  • 沒有已下市公司:主檔只涵蓋目前掛牌的公司。要做無存活者偏誤的研究,缺少已下市標的會系統性高估績效。
  • 產業別為代碼:回傳的是產業代碼(例如 01),代碼與名稱的對應需另行取得。
  • 兩套命名需自行對映:如上,跨市場合併必須自建對映層,並在官方調整欄位時維護。
  • 文字欄位含全形空白與「-」:無資料時以全形破折號表示,需自行清洗。

需要沿革與已下市標的時的做法

研究上真正會出問題的是「已下市公司不見了」。TW Market Data 的上市日線保留了 311 檔已停止交易股票的完整價史(其中 262 檔帶有官方下市日期),公司主檔與產業分類以同一套 schema 提供,上市上櫃不必自行對映。

curl "https://api.twmarketdata.com/v2/datasets/security-master?symbol=2330" \
  -H "X-API-Key: $TWMD_API_KEY"

相關連結