2020 年,這個網址上是一篇三行程式的小練習:用 pandas_datareader 抓台股加權指數的歷史股價,算 60 日均線,再用 cumprod() 做出「收盤站上均線就持有」的簡易回測。六年後重跑,同一段程式連 import 都過不了。這次改版做了三件事:實測 pandas-datareader 在 2026 年壞掉的兩種方式、給出 yfinance 與 finlab 兩條可以跑的替代路徑,最後把舊回測延長到 26 年、補上交易成本,並用含下市股票的完整股池,量化舊抓法藏在背後的生存者偏差。
![]()
重跑 26 年的關鍵數字
| 項目 | 數值 |
|---|---|
| pandas-datareader 維護現況 | 最後版本 0.10.0(2021 年 10 月),Yahoo 讀取已失效 |
| ^TWII 收盤 > 60 日均線擇時(2000-01~2026-06),無成本 | 年化 10.24%、日夏普 0.83、MDD -22.54% |
| 同一策略,含手續費與證交稅 | 年化 6.36%、日夏普 0.55、MDD -36.84% |
| 買進持有 ^TWII(同期間) | 年化 6.36%、日夏普 0.41、MDD -66.22% |
| 26 年進出次數/持有時間占比 | 162 次/60.4% |
| 生存者偏差:僅現存股池 vs 含下市全股池(2007-07~2026-06) | 年化高估 0.32 個百分點、總報酬高估 15 個百分點 |
| 同期 0050 含息買進持有 | 年化 13.61%、日夏普 0.73、MDD -55.75% |
| 加權指數「價格指數 vs 報酬指數」年化差(2003~2026) | 3.95 個百分點(股息的重量) |
| 台積電原始收盤價 vs 還原股價的長期報酬差(2007~2026) | 1.98 倍 |
資料快照:2026-06-09。表中每個數字都能用文末的完整回測腳本重現,權益曲線原始數據可下載 data.csv。
pandas-datareader 在 2026 年壞掉的兩種方式
舊文的核心程式只有一行:
顯示程式碼
from pandas_datareader import data
df = data.DataReader("^TWII", "yahoo", "2000-01-01", "2018-01-01")2026 年 6 月,我們在兩種環境下重跑。第一種是現代環境(pandas 3.0.2):pandas_datareader 0.10.0 還在用 pandas 已改掉的內部 API,連匯入都會失敗:
顯示程式碼
TypeError: deprecate_kwarg() missing 1 required positional argument: 'new_arg_name'第二種是把 pandas 降版到 2.1.4,匯入成功,但 Yahoo 在 2022 年前後改掉了舊的歷史報價端點,讀取一樣失敗:
顯示程式碼
RemoteDataError: Unable to read URL:
https://finance.yahoo.com/quote/^TWII/history?period1=...&period2=...兩個錯誤對應同一個事實:pandas-datareader 的最後一個版本 0.10.0 發佈於 2021 年 10 月,之後專案實質停止維護,Yahoo 讀取器沒有人修。如果你照著 2022 年以前的教學安裝它然後報錯,問題不在你的程式,在套件本身。舊文原本附的 Colab 筆記本含有同一段失效程式碼,本次改版已移除,改提供文末的新版腳本。
2026 年用 Python 抓歷史股價的三條路

第一條路是 yfinance。它走 Yahoo 新版 API、持續維護中,抓大盤指數一次到位,^TWII 的日資料可以回溯到 1997 年:
顯示程式碼
import yfinance as yf
twii = yf.Ticker("^TWII").history(start="2000-01-01", auto_adjust=False)
close = twii["Close"]更完整的用法(八大全球指數、還原股價、批次下載)寫在 yfinance 教學。
第二條路是 finlab。差別在資料的完整性:yfinance 抓台股個股要一檔一檔列代號,而且「今天還查得到的代號」清單裡沒有已下市的股票;finlab 的 data.get() 一次回傳全市場的價格矩陣,其中包含已下市標的(以 2026-06 的資料快照計,全市場 2,752 個代號中有 369 檔已超過 90 天沒有報價,仍完整保留歷史價格),這是後面量化生存者偏差的關鍵:
顯示程式碼
# pip install finlab,首次使用會自動引導登入
from finlab import data
close = data.get("price:收盤價") # 全市場日收盤,含已下市股票
adj_close = data.get("etl:adj_close") # 還原股價,回測請用這個第三條路其實沒有換路:pandas 本身的 rolling、shift、cumprod 心法完全不變,換掉的只有資料來源那幾行。Series 與時間序列的基本功,可以在 pandas 時間序列教學補齊;如果你還在猶豫要不要走程式這條路,用 Python 投資的好處有整體的比較。
均線與一行回測的數學,六年後仍然成立
60 日簡單移動平均(SMA)的定義:
其中 是第 天的收盤價。pandas 寫成 close.rolling(60).mean(),前 59 天因樣本不足回傳 NaN;舊文用 min_periods=1 強制從第一天就開始計算,本次改版改用預設值,讓前 59 天沒有訊號,差異只影響 2000 年的前三個月。
用一個虛構的小例子走一遍(以下數字純屬示範)。假設「示範食品」連續五天的收盤價是 100、102、101、103、104,取 3 日均線:第 3 天的 SMA 是 (100+102+101)/3 = 101,第 4 天是 (102+101+103)/3 = 102,第 5 天約 102.67。規則「收盤站上均線就持有」在第 4 天首次成立(103 > 102),於是以 103 的價格進場,第 5 天漲到 104,這一天的持有報酬是 104/103 − 1 ≈ 0.97%,權益曲線從 1 變成約 1.0097。
舊文的「一行回測」(c.shift(-1) / c)[signal].cumprod(),本質就是把每個持有日的成長率連乘:
是第 天收盤的訊號(1 = 持有、0 = 空手), 是隔日報酬。這條式子隱含一個邊界假設:訊號在收盤價計算完成的同一瞬間,就以同一個收盤價成交。實務上你看到收盤站上均線時市場已收盤,比較保守的做法是隔天才進場;本次改版用 signal.shift(1) 把訊號與報酬錯開一天,消除這個模糊地帶。回測的整體概念與更多常見陷阱,回測是什麼有系統性的整理。
實驗 A:把 2020 年的回測延長到 26 年,補上成本
新版核心程式如下,一行一個動作:
顯示程式碼
sma60 = close.rolling(60).mean()
signal = close > sma60 # 收盤站上 60 日均線
position = signal.shift(1).fillna(False) # 隔一天才持有,避免前視
daily_return = close.pct_change().fillna(0)
strategy_return = daily_return.where(position, 0)
equity = (1 + strategy_return).cumprod() # 權益曲線成本的處理方式:進場日扣 0.1425% 手續費,出場日扣 0.1425% 手續費加 0.3% 證交稅(以買賣追蹤大盤的 ETF 來模擬,一次來回約 0.585%)。結果分三個窗口看:

| 區間 | 版本 | 總報酬 | 年化 | 日夏普 | MDD |
|---|---|---|---|---|---|
| 2000~2017(舊文窗口) | 擇時,無成本 | +302.5% | 8.05% | 0.68 | -22.39% |
| 2000~2017 | 擇時,含成本 | +116.3% | 4.38% | 0.40 | -26.38% |
| 2000~2017 | 買進持有 | +21.5% | 1.09% | 0.16 | -66.22% |
| 2018~2026 | 擇時,無成本 | +226.4% | 15.06% | 1.15 | -22.54% |
| 2018~2026 | 擇時,含成本 | +136.1% | 10.73% | 0.85 | -26.52% |
| 2018~2026 | 買進持有 | +320.0% | 18.55% | 1.02 | -31.63% |
| 2000~2026(全段) | 擇時,無成本 | +1213.7% | 10.24% | 0.83 | -22.54% |
| 2000~2026 | 擇時,含成本 | +410.6% | 6.36% | 0.55 | -36.84% |
| 2000~2026 | 買進持有 | +410.5% | 6.36% | 0.41 | -66.22% |
三個觀察。第一,舊文的圖沒有畫錯:在 2000~2017 這個包含網路泡沫與金融海嘯的窗口,均線擇時即使扣掉成本(年化 4.38%)仍然大勝買進持有(1.09%),因為它兩次崩盤都在場外。學術文獻也有對應:Brock, Lakonishok & LeBaron (1992) 對道瓊指數 90 年的資料檢驗均線規則,發現買進訊號之後的平均報酬顯著高於賣出訊號之後,是「均線訊號有資訊」這一派最常被引用的證據。
第二,2018 年之後劇本反轉:台股走了一段罕見的長多,擇時的代價是每次洗盤都被甩下車再追回來,含成本年化 10.73%,輸給買進持有的 18.55%。把 26 年合起來看,含成本擇時的年化報酬與買進持有完全相同(都是 6.36%),剩下的差別只有風險:最大回撤從 -66.22% 降到 -36.84%,日夏普從 0.41 升到 0.55。這個「報酬不變、回撤減半」的結果,跟 Sullivan, Timmermann & White (1999) 的提醒一致:他們用 bootstrap 修正資料窺探(data snooping)之後,技術規則的超額報酬大多消失,能留下的通常是風險面的改善而非報酬面的勝出。
第三,成本是主角。26 年共 162 次進出,每次來回約 0.585%,複利下來權益只剩無成本版本的 39%(13.14 倍變 5.11 倍)。任何高頻進出的訊號,回測時沒扣成本都會嚴重失真。另外要交代一個基準的限制:^TWII 是價格指數,不含股息。同期間(2003~2026)發行量加權「報酬指數」的年化比價格指數高 3.95 個百分點;擇時策略只有 60.4% 的時間在場內,粗估每年會比買進持有少領約 1.6 個百分點的股息(3.95 × 39.6%),所以含息口徑下,擇時的相對表現會比上表更難看。想把均線濾網與融資維持率、VIX 等其他指標組合著用,可以看四種均線指標濾網實測;雙均線交叉的版本在比特幣均線交叉回測有完整實作。
實驗 B:生存者偏差值多少?把 248 檔下市股票放回股池
舊文當年自己承認「下市股票的股價沒辦法取得,會有生存者偏差」,但沒有人量過這個偏差在台股值多少。生存者偏差的定義:
只用「活到今天」的股票估計報酬,會比用完整母體估計來得高,因為被剔除的多半是下市前先跌一大段的公司。Brown, Goetzmann, Ibbotson & Ross (1992) 證明了這件事的一般性:只要樣本以「存活」為條件,績效研究就會系統性高估報酬,連「報酬持續性」這種型態都可能是偏差製造出來的假象。
實驗設計:同一條「還原價站上 60 日均線就等權持有」的規則,套到台股全部普通股(4 碼數字代號、排除 ETF、權證與 TDR,共 2,218 檔),每週再平衡,用 finlab 的 sim() 內建台股成本回測。跑兩次:一次用含 248 檔已下市股票的完整股池,一次只用 2026 年 6 月還活著的 1,970 檔(這就是用 yfinance 拿今日代號清單回測時,不自覺採用的股池):
顯示程式碼
from finlab import data
from finlab.backtest import sim
adj_close = data.get("etl:adj_close")
# 台股普通股:4 碼數字、非 0 開頭(排除 ETF、權證、TDR)
stocks = [s for s in adj_close.columns
if s.isdigit() and len(s) == 4 and not s.startswith("0")]
adj_close = adj_close[stocks]
# 同一條規則:還原價站上 60 日均線就等權持有,每週再平衡
sma60 = adj_close.rolling(60).mean()
position = (adj_close > sma60).fillna(False)
report = sim(position, resample="W", upload=False)
| 股池(2007-07~2026-06) | 總報酬 | 年化 | 日夏普 | MDD | 平均持股 |
|---|---|---|---|---|---|
| 含下市股票的全股池 | +162.9% | 5.25% | 0.38 | -68.52% | 702 檔 |
| 僅現存股池(剔除下市) | +178.3% | 5.57% | 0.40 | -67.51% | 665 檔 |
| 0050 含息買進持有 | +1012.4% | 13.61% | 0.73 | -55.75% | 1 檔 |
全股池的互動式回測報告可以操作看看:
結果有兩層。第一層是偏差本身:剔除下市股票讓同一個策略的年化報酬從 5.25% 變成 5.57%,高估 0.32 個百分點,19 年累積下來總報酬高估 15 個百分點。這個數字比文獻裡常見的估計溫和,原因是本實驗平均持有 702 檔、等權分散,單一檔下市的殺傷力被稀釋了;持股越集中、越偏好低價弱勢股的策略,這個偏差會越大,0.32 個百分點應視為下限而非通則。
第二層更重要:不管用哪個股池,「均線之上就買」都遠輸給 0050 含息買進持有(年化 5.25%~5.57% 對 13.61%),回撤還更深。這條規則當教材很好,當策略不及格;它的問題不需要等生存者偏差來放大。對選股因子有興趣的話,台股選股方法的系統性回測把基本面、技術面、籌碼面因子放在同一框架下比較,是更合理的起點。
還原股價:比生存者偏差更大的資料地雷
實驗 B 用的是 etl:adj_close 還原股價,不是原始收盤價,這個選擇本身就值一節。台股上市公司配息時股價會跳空下修,原始收盤價看起來像下跌,實際上股東拿到了現金;不還原的話,所有配息都被當成虧損。

以台積電為例,2007-04 到 2026-06 之間,用原始收盤價計算的累積報酬,與用還原股價計算的相差 1.98 倍。大盤層級也一樣:加權「價格指數」與加權「報酬指數」的年化差距是 3.95 個百分點。換句話說,資料來源選錯欄位,影響比生存者偏差(0.32 個百分點)大一個數量級。yfinance 的 auto_adjust=True 可以處理除權息,finlab 直接提供 etl:adj_close;無論用哪一條路,回測請一律用還原價格。
回測方法與限制
| 項目 | 本文的處理 |
|---|---|
| 交易成本 | 實驗 A:進場 0.1425%、出場 0.1425% + 0.3% 證交稅,手動扣除;實驗 B:finlab sim() 預設值(手續費 0.1425%、賣出證交稅 0.3%) |
| 滑價 | 未假設(0)。實際衝擊取決於資金規模與成交量,本文未估算容量 |
| 股票池 | 實驗 A 為大盤指數本身;實驗 B 為台股普通股 2,218 檔(4 碼數字代號,含 248 檔已下市) |
| 流動性過濾 | 未做。實驗目的是量化資料偏誤而非提出可交易策略;平均持有 702 檔,其中小型股實單未必買得到 |
| 排除類別 | ETF、權證、TDR 以代號規則排除;金融股、KY 股未排除(兩個股池用同一準則,偏差比較不受影響) |
| 前視偏差 | 訊號用 t 日收盤、持有 t+1 日報酬;只用價格資料,無財報公告日對齊問題。「現存股池」本身是刻意引入的前視,這正是實驗要測量的對象 |
| 權重 | 實驗 A 為全倉進出;實驗 B 等權、無單檔上限 |
| 周轉率 | 實驗 A 為 26 年 162 次進出;實驗 B 每週再平衡,平均周轉率未統計(未做) |
| 樣本內外 | 規則固定(60 日、無參數搜尋),全段一次跑完;因為沒有調參,未另切樣本外(未做) |
基準口徑補充:^TWII 為價格指數,不含息,對買進持有相對不利;0050 基準為 etl:adj_close 還原價的純指數算術買進持有,不含交易成本,而兩個策略股池經 sim() 扣足成本,比較口徑對策略偏嚴格。
從三行練習到可以信的回測
這篇文章的兩個實驗,從下載資料、對齊下市股票、扣成本到產出報告,全部用 pip install finlab 完成;你可以拿文末的腳本改成自己的訊號,跑出同一格式的報告。如果想把這套流程放進更完整的脈絡,量化交易完整指南從觀念到上手路徑都有整理,想往自動化執行走的話,程式交易教學總覽涵蓋了從回測到下單的全流程;對 Python 還不熟的讀者,可以從股票分析入門指南開始。
常見問題
pandas-datareader 現在還能用嗎?
抓 Yahoo 股價已經不行了。套件最後版本 0.10.0 停在 2021 年 10 月,在 pandas 3.x 環境連匯入都會失敗;降到 pandas 2.x 可以匯入,但 Yahoo 讀取器因端點改版而報 RemoteDataError。它的部分讀取器(如 FRED、World Bank)仍可運作,但 Yahoo 路線請改用 yfinance。
為什麼我照舊教學跑,import pandas_datareader 就報錯?
你的 pandas 版本太新。pandas_datareader 0.10.0 引用了 pandas 內部已改署名的 deprecate_kwarg,在 pandas 3.x 直接拋出 TypeError。這不代表你裝錯,網路上 2022 年以前的教學(包含本文的舊版)都會遇到同樣問題。
yfinance 可以抓台股個股嗎?
可以,代號加上 .TW(上市)或 .TWO(上櫃),例如 yf.Ticker("2330.TW")。限制有二:一次只能列舉你知道的代號,拿不到已下市股票的歷史價格,回測整個市場時就會踩進本文實驗 B 的生存者偏差;台股特有的欄位(月營收、財報、籌碼)也沒有。指數與美股用 yfinance 很順手,台股全市場研究建議用 finlab 的資料。
生存者偏差實際影響有多大?
看策略而定。本文的設定(等權、平均持有 702 檔)量出年化 0.32 個百分點、19 年總報酬 15 個百分點的高估;持股越集中、越偏好財務危機邊緣的低價股,偏差越大。學術上 Brown 等人在 1992 年就指出,以存活為條件的樣本會系統性高估績效,連報酬持續性都可能是假象。
60 日均線擇時值得實際操作嗎?
以本文 26 年的實測,含成本後年化報酬與買進持有相同(6.36%),優勢只剩回撤減半(-36.84% 對 -66.22%)與較高的夏普。如果你的目標是控制回撤,它有參考價值;如果目標是超額報酬,這條規則在含息口徑下會輸給買進持有。把它當成風險管理工具而非報酬來源,比較符合數據。
回測該用收盤價還是還原股價?
一律用還原股價。台積電 2007~2026 的例子顯示,兩者的累積報酬相差 1.98 倍;不還原等於把每一次配息都記成虧損。yfinance 用 auto_adjust=True,finlab 用 etl:adj_close。
舊版文章的三行回測程式還值得學嗎?
值得,這正是我們保留這篇文章的原因。rolling、shift、cumprod 三個動作構成了向量化回測的最小骨架,本文的數學那一節給出了它對應的公式。壞掉的只是資料來源那幾行,pandas 本身的心法沒有過期。
延伸閱讀
- 量化交易完整指南:從零開始的系統化路徑
- 程式交易是什麼:回測、訊號到自動下單的全流程
- yfinance 教學:免費下載全球指數與股價:本文替代方案的完整版
- Python 時間序列實做:rolling 與 shift 的基本功
- 台股選股回測:單因子與複合策略:比均線更系統的選股研究
- LPPL 泡沫模型實測:同樣需要長期指數資料的擇時研究
- 量化名詞詞彙表:夏普比率、最大回撤、回測等定義
下載資源
| 檔案 | 說明 |
|---|---|
| strategy.py | 雙實驗完整程式碼(yfinance + finlab) |
| data.csv | 實驗 B 三條權益曲線原始數據 |
投資警語:本文僅供教學參考,不構成投資建議。過去績效不代表未來表現,投資有風險,請審慎評估自身風險承受能力。
最後更新:2026-06|回測區間:2000-01~2026-06(SMA60 擇時)、2007-07~2026-06(生存者偏差實驗),資料快照 2026-06-09|作者:FinLab 量化研究團隊(經量化研究員審閱)
FinLab AI
想建立自己的策略?
用自然語言描述你的選股想法,AI 自動驗證、回測、給你答案
免費開始