台股的免費資料源不少。券商 API、政府開放資料、幾個做得不錯的第三方套件,裝一裝就有日線和財報可以用。
我還是自己爬了 868 萬列。
倒不是因為喜歡跟交易所的限流器搏鬥。是有四件事,用現成的拿不到,而其中三件會讓回測失真,你還不會發現。
大部分現成資料源的做法是:先拿一份股票清單,再逐檔去查歷史。
那份清單是現在的清單。
我把 20 年日線攤開來數,只算普通股(權證、ETF、存託憑證都排除):
20 年間有過成交紀錄的普通股 2,241 檔
最近一個月仍有報價 1,978 檔
已經沒有報價 263 檔(11.7%)

用今天的清單去回溯 2012 年,你會漏掉那年 1,463 檔裡的 147 檔。每十檔漏一檔,年代越早漏得越多。
如果那 10% 跟其他 90% 差不多,其實無所謂。問題是不一樣。
我算了每檔股票「消失前最後 12 個月」的報酬。有足夠歷史可以算的下市公司是 239 檔:
中位數 跌超過 50% 的比例
消失的 239 檔 +1.8% 24.3%
仍在市場的 1,898 檔 +0.6% 2.1%
中位數差不多,但崩跌的比例差了十二倍。
存活者偏差漏掉的不是平均值,是災難。四分之一的下市公司在消失前一年腰斬過,而你的回測裡沒有它們,所以你的策略從來不必面對那種情況。
(這個對照不算嚴謹。消失的公司各自有不同的「最後 12 個月」,仍在市場的則是同一段近期。不過尾巴差十二倍,不是這種偏差能解釋的。)
逐日抓全市場就沒這個問題。每天抓一次當天的收盤行情表,那天在市場上的股票自然都在裡面,包括三個月後要下市的那些。清單變成資料的結果,不是輸入。
順帶一提,這樣抓也比較省。逐檔逐月查要幾十萬次請求,逐日抓上市是五千次左右。
現成 API 幾乎都不給的一個欄位:這筆數字,哪一天才公開?
API 告訴你「2024 年 1 月營收 2,157.9 億元」。它不會說這個數字要到 2024 年 2 月 10 日才公布。
少了這欄,你就沒辦法從架構上保證回測沒偷看未來,只能每次寫程式的時候自己記得。
這件事值多少我量過。同一個月營收訊號,只把下單時點從「營收公布後」改成「營收月底」,提早十天,年化報酬從 5.23% 變成 24.33%,夏普 0.34 變 1.05。而且偷看那版的最大回落還比較小,績效表上看不出任何異常。
十天。所以每筆資料都得帶著「哪天才拿得到」,而這只能在爬的時候順手記下來。
這條最平凡,但它決定你能不能做研究。
回測不是跑一次。一個特徵要試十種算法,一個參數要掃十個值,改一次程式要重跑全部歷史。免費 API 一天幾百到幾千次請求,跑兩輪就沒了,然後等明天。
自己爬完,資料躺在本機的 parquet 檔裡。20 年日線 868 萬列、幾百 MB,讀進記憶體幾秒鐘。想跑幾次跑幾次。
前三條是老生常談。第四條是我今天才又被上了一課的。
做資料品質檢查的時候,我看到還原股價裡有一堆 +inf% 的單日報酬。那代表前一天的價格是 0。
追下去是這個:

2011-11-08 8077 收盤 6.98 成交量 1000
2011-11-09 8077 收盤 0.00 成交量 0 ← 沒有成交
2011-11-10 8077 收盤 0.00 成交量 0 ← 沒有成交
2011-11-14 8077 收盤 NaN 成交量 0 ← 同樣沒有成交
2011-11-15 8077 收盤 6.91 成交量 1000
上櫃的收盤行情表對「當日無成交」有兩種寫法。我一開始以為是年代差異,翻了原始回應才發現,這兩種寫法出現在同一個月裡:
交易所原始回應(8077,2011 年 11 月)
11/09 "0.00" 11/14 "--"
11/10 "0.00" 11/16 "--"
11/11 "0.00" 11/17 "--"
我的清洗規則認得 --,會轉成 NaN。0.00 是合法數字,沒有任何規則會攔它。於是「這天沒人交易」變成「這天股價是 0 元」,還原股價照樣把它乘上調整係數,跨過這天的報酬計算就是 ±inf 或 −100%。
規模是 112,356 列,佔全部日線的 1.29%,全部集中在上櫃 2008 到 2011 年。
它不報錯。程式跑完了,資料看起來也正常,除非你特地去找。
這才是第四條的重點。
我的爬蟲有個設計:所有網路回應,在解析之前先原封不動 gzip 存進 data/raw/。
所以發現 parser 有問題的時候,我做的是:改 parser(加三行,把 OHLC 全為 0 的列的價格設成 NaN),然後用同一個指令重跑,快取直接命中。
連網抓取 3.1 秒/天
離線重解析 0.1 秒/天 快 30 倍
4,869 個交易日約 6 分鐘,網路請求 0 次
修完之後:
殘留的 0 元價格 112,356 → 0
無法解釋的單日跳空 0.828% → 0.120%
如果沒存原始回應,這件事的代價是重新對 TPEx 發 4,869 次請求、等四個小時,還要祈禱那幾天交易所沒改版沒擋我。實務上大部分人會選擇「算了,把異常值濾掉就好」,然後 bug 永遠留在資料裡。
現成 API 沒辦法這樣做,因為你拿到的已經是別人解析過的結果。別人的 parser 有沒有犯同樣的錯,你不知道,也修不了。