解答
What are t187ap03_L and mopsfin_t187ap03_O?
These two endpoints are the same dataset for two markets: t187ap03_L carries listed-company profiles and mopsfin_t187ap03_O carries OTC ones. Both give company name, abbreviation, industry code, business number, chairman, general manager, spokesperson and address. The catch is the naming — the listed endpoint uses Chinese field names, the OTC one English.
What these endpoints are
A company profile is the master record that maps a ticker to a real entity — you need it before any cross-dataset analysis, to resolve codes, industries and names. The listed version sits on the TWSE domain and the OTC version on the TPEx domain; probed on 2026-07-31 they returned roughly eleven hundred and nine hundred rows respectively.
# listed
curl "https://openapi.twse.com.tw/v1/opendata/t187ap03_L"
# OTC
curl "https://www.tpex.org.tw/openapi/v1/mopsfin_t187ap03_O"Field mapping: one dataset, two vocabularies
This is the part worth planning for. The two payloads mean nearly the same things but name them in different languages, so concatenating the arrays gives you two incompatible sets of keys. The main correspondences:
- 公司代號 (company code) maps to SecuritiesCompanyCode
- 公司名稱 (full name) maps to CompanyName
- 公司簡稱 (short name) maps to CompanyAbbreviation
- 產業別 (industry) maps to SecuritiesIndustryCode
- 營利事業統一編號 (business number) maps to UnifiedBusinessNo. — the trailing period is part of the key
- 董事長 maps to Chairman, 總經理 to GeneralManager, 發言人 to Spokesman
- 出表日期 maps to Date, both ROC-calendar strings
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, is_listed):
if is_listed:
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))Its limits
- Current state only, no history: the payload is today's master record. Renames, reclassifications and officer changes show only their latest value, so you cannot reconstruct how a company looked at an earlier date.
- No delisted companies: only currently traded names appear. Research that ignores delisted names carries survivorship bias and systematically overstates performance.
- Industry arrives as a code: you get a numeric industry code, not a label, and the code-to-name mapping comes from elsewhere.
- Two vocabularies to reconcile: cross-market work needs a mapping layer you own and maintain as the official formats change.
- Padding and placeholder characters: absent values are marked with a full-width dash, and text fields carry full-width spaces, so clean before parsing.
When you need history or delisted names
The failure mode that actually breaks research is the missing delisted companies. TW Market Data retains full price history for 311 names that have stopped trading, 262 of them carrying an official delisting date, and serves company master data and industry classification in the same schema across both markets, so there is no mapping layer to maintain.
curl "https://api.twmarketdata.com/v2/datasets/security-master?symbol=2330" \
-H "X-API-Key: $TWMD_API_KEY"相關連結