上櫃三大法人與逐檔當沖可以回溯到 2018 年,端點藏在網頁版而不是 OpenAPI。怎麼找、欄位名稱重複七組怎麼讀、HTTP 520 跟 302 各代表什麼。
發布於 2026-09-06・約 5 分鐘
證交所(上市)的歷史資料好拿,網路上教學一堆。櫃買中心(上櫃)就不是了。
它的 OpenAPI 看起來很完整,兩百多個端點,但幾乎每一個都只回最新一個交易日。你抓不到上週三的資料,更抓不到 2019 年的。
一般的結論會是「櫃買沒有開放歷史資料」。這是錯的。歷史端點存在,只是不在 OpenAPI 底下,在網頁版的 API 裡,而且吃日期參數。
2026-09-04 實測確認,兩個都能回溯到 2018 年。
上櫃三大法人(逐檔)
https://www.tpex.org.tw/www/zh-tw/insti/dailyTrade?type=Daily§=EW&date=YYYY%2FMM%2FDD&id=&response=json
可回溯至 2018 年 3 月(2018-02-15 那天回空)。2019-08-20 有 587 檔,2026-09-03 有 900 檔,檔數隨市場成長,看起來是完整的。
上櫃逐檔當沖
https://www.tpex.org.tw/www/zh-tw/intraday/stat?type=Daily&date=YYYY%2FMM%2FDD&id=&response=json
至少能回到 2018 年 6 月。回傳裡 tables[0] 是全市場摘要,tables[1] 才是逐檔。
tables[0]
tables[1]
日期是民國格式,而且斜線要編碼:115/09/03 就是 2026-09-03,放進網址是 115%2F09%2F03。
115/09/03
115%2F09%2F03
def roc_date(d): """datetime.date -> '115/09/03'""" return f"{d.year - 1911}/{d.month:02d}/{d.day:02d}"
直接猜 URL 很浪費時間。有效的順序是:
一、先翻 swagger。
curl -s https://www.tpex.org.tw/openapi/swagger.json | jq -r '.paths | keys[]'
兩百多個端點。這裡找不到你要的歷史資料,但可以找到報表的正式名稱,拿那個名稱去對網頁版的功能選單,就知道該去哪一頁。
二、看網頁版的網路請求。 櫃買的網頁是 SPA 殼,抓 HTML 原始碼找不到任何 API 呼叫,全部是 JS 執行後才發出的。要開瀏覽器的開發者工具看 Network 分頁。自動化的抓取工具在這裡也會失效,我用的 WebFetch 是直接被 403 擋掉。
三、用 HTTP 狀態碼判斷路徑存不存在。 這條是最省時間的:
520 是 Cloudflare 的狀態碼。櫃買會間歇性回它,跟時段沒有明顯關係。我第一次拿到 520 的時候以為端點錯了就放棄了,其實那正是「找對了」的訊號。
所以抓取一定要有重試:
import time, requests def fetch(url, tries=5): for i in range(tries): r = requests.get(url, timeout=20, headers={"User-Agent": "Mozilla/5.0"}) if r.status_code == 200: return r.json() if r.status_code == 302: raise ValueError(f"路徑不存在(302 導走):{url}") time.sleep(2 ** i) raise RuntimeError(f"重試 {tries} 次仍失敗:{url}")
重試用完要拋例外,不要 return None。 這個端點會間歇性失敗,如果失敗是安靜的,你會得到一份「有些日子沒資料」的序列,而且分不出來是那天真的沒開盤還是抓失敗。
這是最需要小心的地方。三大法人那個端點回 24 個欄位,名稱重複七組:外資、投信、自營各自都有「買進股數」「賣出股數」「買賣超股數」,名稱一模一樣。
用名稱讀會拿到錯的欄位(或只拿到最後一個)。只能用位置讀。
而位置沒有文件保證,所以要自己驗。用恆等式:
def verify_positions(row): """用會計恆等式確認欄位位置沒跑掉。""" foreign_ex_dealer = int(row[4]) # 外資(不含自營)買賣超 trust = int(row[13]) # 投信買賣超 dealer_self = int(row[16]) # 自營商(自行買賣) dealer_hedge = int(row[19]) # 自營商(避險) dealer_total = int(row[22]) # 自營商合計 three_total = int(row[23]) # 三大法人合計 assert dealer_self + dealer_hedge == dealer_total, "自營商兩臂加不起來" assert foreign_ex_dealer + trust + dealer_total == three_total, "三大法人加不起來"
每一批抓下來都跑一次這個檢查,不要只在寫程式那天跑。欄位順序是對方隨時可以改的東西,改了之後你的程式不會噴錯,只會開始算出錯的數字。
當沖那個端點反而單純,欄位名稱唯一,可以按名稱讀。它的驗證方式是比對加總:tables[0] 的全市場總量應該等於 tables[1] 逐檔的加總,我實測差 0。
當沖那份給的是「當日沖銷成交股數」,不是比率。要算比率得自己除:
當沖比率 = 當日沖銷成交股數 ÷ 當日總成交股數 × 100
分母(總成交量)要從日 K 線那邊拿,不在這個端點裡。這件事沒寫在任何地方,是比對數字對不上才發現的。
上櫃股票在很多策略的訊號裡佔比不低。我實測過自己那套訊號,66.8% 落在上櫃,而籌碼面的資料涵蓋率是 0%——因為我原本只接了證交所的端點。
那等於三分之二的訊號在做決策時少了一整個維度。更麻煩的是,我原本以為某個籌碼濾網是有效的(那是用上市資料驗出來的),套到上櫃反而是反效果。「這個規則有效」的結論,只在你驗證過的那個母體裡成立,換一個市場就要重驗一次。
補上櫃資料的成本大概是一天。發現自己一直在對三分之二的樣本做錯誤假設,那個成本高很多。
標籤:台股、櫃買中心、TPEx、API、Python、資料源