跳至主要內容
台股研究 約 9 分鐘閱讀

Python 股票選股回測教學:5 分鐘寫出條件,
五種篩選全輸 0050 實測

Python 股票選股與回測教學:5 分鐘用 finlab 把本益比、月營收年增、ROE、站上均線寫成程式並回測。真實數字:五種直覺篩選 2018 至 2026 年化最高 17.7%、最差 1.4%,全部輸給 0050 含息的 24.8%。本文以反面教材示範怎麼讀回測結果、條件越加越多為什麼反而更差。

直覺篩選全輸 0050

Python 股票選股回測:五種直覺篩選條件 2018–2026 年化報酬與最大回撤對照,最佳 17.7% 仍輸 0050 含息 24.8%

依本文重跑

執行這個策略

複製提示詞,讓 AI 依照上方證據的資料、條件與風險檢查重跑一次。

告訴你的AI:

幫我設定 FinLab,重現這篇文章的月營收年增 + ROE + 趨勢選股回測,請讀:https://finlab.finance/setup?relatedUrl=/blog/python-stock-selection-backtest-finlab

免安裝,開啟 FinLab Studio

五種直覺選股條件與 0050 含息的年化報酬與最大回撤對照(2018-01 至 2026-08,月頻換股,含手續費與證交稅)。

先講結果:本文示範的五種選股條件,從「本益比低於 15 加上營收成長」到「10 日均線最強的前 20%」,在 2018 年 1 月到 2026 年 8 月這段台股行情裡,年化報酬最高 17.7%、最差 1.4%,全部輸給同期 0050 買進持有含息的 24.8%。這篇文章是一份反面教材。你在 5 分鐘內能學會的,是把腦中的選股條件寫成 finlab 程式、跑出回測、看懂它輸在哪裡;你學不到的,是一套可以照抄的獲利策略,因為這五種都不合格。

這篇專注在最短路徑:一個條件、幾行程式、一張回測報告。想把選股升級成三因子排名、處理前視偏差與波動加權的完整工程流程,請看 Python 量化交易完整教學;只想用三行程式碼試試創新高動能,請看 三行程式碼打造創新高動能策略。三篇分工明確:本文教「把條件寫成程式並讀懂結果」,前者教「把策略做對」,後者教「用最少的程式碼起手」。

五種篩選的成績單

策略(2018-01 至 2026-08,月頻換股,含成本) 年化報酬 日夏普 月索提諾 最大回撤 平均持股
營收年增 > 20% + ROE > 8% + 站上 60 日線(無本益比上限,本文示範) 17.7% 0.70 1.11 -47.8% 14.5 檔
本益比 < 25 + 上述條件 + 趨勢 6.3% 0.36 0.59 -55.1% 9.3 檔
本益比 < 15 + 上述條件 + 趨勢 1.4% 0.20 0.38 -52.9% 5.1 檔
本益比 < 15 + 上述條件(不看趨勢) 11.8% 0.55 0.89 -45.9% 9.9 檔
2020 舊版:10 日均線在全市場前 20%(等權) 17.0% 0.77 1.07 -38.3% 117.7 檔
0050 買進持有(含息) 24.8% 1.18 1.92 -34.0% 1 檔

回測口徑:股票池限定 60 日平均成交金額大於 5,000 萬元且收盤價高於 10 元,先排除流動性不足的股票;篩選通過者依月營收年增率由高到低取前 30 檔等權持有,每月換股一次;sim() 內扣手續費 0.1425% 與賣出證交稅 0.3%,未另設滑價;0050 基準用 etl:adj_close 還原價計算含息買進持有。累積報酬:示範策略 +306%、舊版 +284%、0050 +580%。所有數字都能用文末的 strategy.py 重跑。

5 分鐘上手:從安裝到第一張回測報告

安裝完成後,整個流程只有四步:取資料、寫條件、排序選股、跑回測。以下是本文示範策略的完整程式,每一行只做一件事。

顯示程式碼
import finlab
from finlab import data
from finlab.backtest import sim
 
finlab.login()
 
# 載入資料
close = data.get('price:收盤價')
volume = data.get('price:成交股數')
rev_yoy = data.get('monthly_revenue:去年同月增減(%)').index_str_to_date().reindex(close.index, method='ffill')
roe = data.get('fundamental_features:ROE稅後').index_str_to_date().reindex(close.index, method='ffill')
 
# 股票池:流動性與價格門檻
amount = (close * volume).rolling(60).mean()
pool = (amount > 50_000_000) & (close > 10)
 
# 選股條件:營收成長、獲利能力、趨勢確認
cond = pool & (rev_yoy > 20) & (roe > 8) & (close > close.rolling(60).mean())
 
# 通過條件者依營收年增率排序,取前 30 檔
score = rev_yoy.where(cond)
position = score.rank(axis=1, ascending=False) <= 30
 
# 月頻換股回測
report = sim(position, resample='M', name='營收年增 + ROE + 趨勢')
report.display()

幾個關鍵細節:

  • data.get() 換資料表名稱就能切換到任何基本面、籌碼面或價格資料,完整清單見 Python 台股資料完整指南。
  • index_str_to_date() 把月營收與財報從「所屬月份」對齊到「實際公告日」,避免用還沒公布的數字選股,也就是避開前視偏差。
  • rank(axis=1) 是橫向排名,每一天在所有通過條件的股票裡排序;<= 30 就是取前 30 檔。
  • resample='M' 代表每月最後一個交易日重新計算持股,中間不動。

想加本益比上限,只要多取一張表、多加一個條件:

顯示程式碼
pe = data.get('price_earning_ratio:本益比')
 
cond_pe = cond & (pe > 0) & (pe < 15)

pe > 0 是為了排除虧損公司的負本益比,否則「本益比小於 15」會把虧損股也放進來。

2020 年舊版的寫法更短,用移動平均線做一個全市場排名(沿用同一組股票池):

顯示程式碼
moving_average = close.rolling(10).mean().where(pool)
top_20_percent = moving_average > moving_average.quantile(0.8, axis=1)
 
report_old = sim(top_20_percent, resample='M', name='10 日均線前 20%')
report_old.display()

這四行就是 2020 年原文的示範,只保留同一組流動性與價格門檻,沒有任何基本面條件;每月平均持有 117.7 檔,等於買進一個「近期股價偏高族群」的等權指數。

五策略的累積淨值與回撤

finlab 回測五種 Python 選股條件 2018–2026 累積淨值對數刻度,0050 含息 +580% 領先示範策略 +306%

累積淨值(對數刻度)。0050 含息累積 +580%,示範策略 +306%,舊版 +284%,本益比小於 15 加趨勢的版本幾乎沒有成長。

五種 Python 選股回測策略 2018–2026 回撤曲線,本益比小於 25 加趨勢最大回撤 -55.1%,0050 為 -34.0%

回撤曲線。加了本益比上限的三個版本最大回撤都超過 -45%,舊版 117 檔分散組合的 -38.3% 是五種裡最淺的,0050 為 -34.0%。

Python 選股回測風險調整指標對照:日夏普、月索提諾、最大回撤,0050 日夏普 1.18 高於五種篩選最高的 0.77

風險調整指標。五種篩選的日夏普介於 0.20 到 0.77,0050 為 1.18;月索提諾最高 1.11,0050 為 1.92。

教訓一:本益比上限越嚴,持股越少,績效越差

把成績單裡的第一列和第二、三列排在一起看,唯一的差別是本益比上限:

本益比上限 平均持股 年化報酬 最大回撤
無 14.5 檔 17.7% -47.8%
< 25 9.3 檔 6.3% -55.1%
< 15 5.1 檔 1.4% -52.9%

「營收成長、獲利好、股價站上均線、本益比又低」聽起來像是四個好條件的交集,實際上四個條件同時成立的股票非常少。本益比壓到 15 以下時,平均只剩 5.1 檔,這時候的組合已經稱不上「策略」,比較像是每個月押注五檔股票。五檔的組合,任何一檔踩到地雷就是 20% 的權重受傷,回撤 -52.9% 就是這樣來的。

從市場邏輯來看,這段台股行情裡營收高成長的公司通常也伴隨高本益比,硬要求「高成長又便宜」,等於把成長股裡最被市場看好的那群排除掉,留下的往往是成長性受到質疑、所以才便宜的公司。Fama & French(1992)在美股資料裡發現低股價淨值比與小市值能解釋橫斷面報酬差異,這篇論文常被引用來支持「便宜有溢價」,原文見 The Cross-Section of Expected Stock Returns;但他們的價值因子是全市場分組後的長期平均,並沒有說「本益比低於 15 的五檔股票會贏大盤」。把學術因子縮成一個門檻、再和其他條件取交集,得到的樣本已經和論文裡的分組完全不同。

教訓二:加一個條件,不等於變得更好

第三列與第四列的差別只有「站上 60 日線」這一個條件:

條件 平均持股 年化報酬 日夏普 最大回撤
本益比 < 15 + 營收年增 + ROE 9.9 檔 11.8% 0.55 -45.9%
同上 + 站上 60 日線 5.1 檔 1.4% 0.20 -52.9%

加上趨勢確認之後,年化從 11.8% 掉到 1.4%,回撤反而更深。直覺上「等股價站上均線再買」應該更安全,但在本益比小於 15 的樣本裡,它做了兩件事:第一,把持股從 9.9 檔砍到 5.1 檔,分散度再度惡化;第二,低本益比的股票之所以便宜,常常是股價已經跌了一段,要求它站上 60 日線等於在它反彈後才追進,而每月換股又會在跌破均線後把它賣掉,一來一回吃掉價差與交易成本。

Jegadeesh & Titman(1993)證實的動能效應,是「過去 3 到 12 個月報酬最強的股票,接下來 3 到 12 個月會繼續領先」,原文見 Returns to Buying Winners and Selling Losers。那是一個跨全市場的相對排名,並且明確避開最近一個月的短期反轉;「收盤價高於 60 日均線」只是單檔股票的絕對條件,兩者不是同一件事。本文示範策略(無本益比上限)雖然也用了 60 日線,但它的持股有 14.5 檔、樣本以成長股為主,趨勢條件才沒有造成同樣的傷害。

結論很簡單:每加一個條件,先看平均持股掉了多少,再看報酬與回撤怎麼變。條件變多、樣本變少、績效變差,是這類篩選最常見的失敗路徑。

教訓三:117 檔的粗糙分散,比 5 檔的精挑細選穩

2020 年的舊版寫法看起來最不講究:沒有任何基本面條件,只在同一組股票池裡看 10 日均線的相對位置。但它交出了五種裡最淺的回撤 -38.3%,日夏普 0.77 也是五種裡最高,年化 17.0% 只比示範策略少約 0.8 個百分點。

原因就在平均持股 117.7 檔。持股分散到上百檔之後,單一股票的意外對整體的影響被稀釋,組合的波動主要來自市場本身。反過來看,本益比小於 15 加趨勢的版本只有 5.1 檔,波動幾乎完全由個股決定。用風險調整報酬來看,日夏普從 0.77 一路降到 0.20,就是分散度逐級惡化的軌跡。

這不代表舊版是好策略。它同樣輸 0050,而且 117 檔的月頻換股意味著可觀的周轉率與交易成本,2020 年原文沒有提到這一點。它只說明一件事:在你還沒有能力挑出真正的贏家之前,分散比精挑更重要。

近三年的數字也支持這個看法。示範策略近 3 年日夏普 1.07、舊版 1.18,都比整段的 0.70 與 0.77 明顯好轉,但這只是後段的局部數字,整段 2018 至 2026 五種全輸 0050 的結論並沒有改變。

教訓四:五種全輸 0050,因為這段行情屬於大型股

五種篩選沒有一種贏過 0050 含息的年化 24.8%、日夏普 1.18、最大回撤 -34.0%。回撤也是,0050 的 -34.0% 比五種篩選全部都淺。

背後的市場事實是,2018 到 2026 年的台股是一段以台積電為核心的大型權值股行情,0050 的成分裡台積電權重極高,等於用一檔 ETF 高比例持有這段時間表現最好的公司。本文五種篩選的股票池是全市場等權,平均持股從 5 檔到 117 檔不等,每一檔權重都很小,即使台積電通過條件,它在組合裡的貢獻也和一檔中小型股沒兩樣。當行情由少數大型股主導,任何等權分散的選股法都會在報酬上落後市值加權的指數。

這也解釋了為什麼營收成長與獲利能力這兩個有學術支持的因子,在這裡沒有帶來超額報酬。Novy-Marx(2013)在美股資料裡發現毛利率高的公司長期報酬顯著較高,並且在控制價值因子之後仍然成立,原文見 The Other Side of Value: The Gross Profitability Premium。但他的獲利能力因子是全市場分組後的多空差異,衡量的是「獲利好的公司相對於獲利差的公司」;本文用 ROE 大於 8% 當門檻、再等權持有前 30 檔,測的是「一小群成長股相對於市值加權大盤」,前者的超額報酬可以存在,後者仍然可以輸給指數。

回測結果怎麼看

表格裡的每一欄都對應一個問題,看懂它們才能判斷一個策略是不是真的比買 0050 好:

  • 年化報酬(CAGR):把整段累積報酬換算成「每年平均複利幾 %」,讓不同長度的回測可以互相比較。本文示範策略 8 年 8 個月累積 +306%,換算年化 17.7%。
  • 日夏普:每承擔一單位波動,換到多少超額報酬,數字越高代表報酬品質越好;用日報酬計算後年化。五種篩選最高 0.77,0050 為 1.18,計算方式與判讀門檻見夏普比率計算工具。
  • 月索提諾:與夏普類似,但分母只算下跌月份的波動,不懲罰上漲的波動。0050 為 1.92,五種篩選最高 1.11。
  • 最大回撤:從歷史高點到後續最低點的最大跌幅,代表你在最壞時間點進場會承受的帳面損失。-47.8% 的意思是資產曾經腰斬近半,多數人撐不到回本。
  • 平均持股:每月平均持有幾檔。低於 10 檔時,回測結果會被少數個股的運氣主導,數字再漂亮也不可靠。

一個實用的判讀順序:先看平均持股夠不夠分散,再看最大回撤能不能承受,最後才看年化報酬有沒有贏過 0050。本文五種篩選連最後一關都沒過。更完整的回測觀念與常見陷阱,見回測是什麼。

下一步:想贏 0050 該往哪裡走

本文的五種篩選都是「條件交集」:符合 A、符合 B、符合 C 就買。這種寫法的問題已經在四個教訓裡攤開:條件越多樣本越少,門檻是硬切的,沒有考慮持股權重,也沒有處理成本。要做出有機會贏過 0050 的版本,方向是把「門檻篩選」換成「排名複合」,把「等權」換成「依風險調整權重」:

常見問題

Python 選股要學多久?

把一個選股條件寫成程式並跑出回測,照本文的四步流程,第一次操作在 5 分鐘內可以完成,程式碼不到 20 行。真正花時間的是學會讀回測結果:本文示範策略年化 17.7% 看起來不錯,但要對照 0050 的 24.8%、-47.8% 的最大回撤與 14.5 檔的平均持股,才知道它不合格。

finlab 回測免費嗎?

本文的程式碼用免費帳號就能執行,finlab.login() 會自動引導登入。免費帳號可以取得的資料期間與資料表數量有限制,部分資料表與完整歷史區間需要 VIP 權限,本文的五種篩選用到的收盤價、成交股數、月營收年增率、ROE 與本益比都是常用資料表。

為什麼我的選股策略輸 0050?

本文五種篩選給了三個具體原因:第一,條件交集讓持股太少,本益比小於 15 加趨勢的版本平均只剩 5.1 檔,年化 1.4%;第二,加條件不等於變好,加上 60 日線確認後年化從 11.8% 掉到 1.4%;第三,2018 到 2026 的台股由大型權值股主導,等權分散的選股法在報酬上天生落後市值加權的 0050。要贏 0050,需要換成排名複合與風險加權的做法,而且要接受多數版本回測完仍然會輸的事實。

延伸閱讀

下載與重現

檔案 說明
strategy.py 五種篩選與 0050 基準的完整回測腳本
metrics.json 本文所有績效數字的來源檔
data.csv 五種策略與 0050 的每日淨值

回測方法與限制:股票池為 60 日平均成交金額大於 5,000 萬元且收盤價高於 10 元;月營收與 ROE 以公告日對齊;手續費 0.1425%、賣出證交稅 0.3% 內扣,未計滑價,實際成交價會比回測更差;五種篩選全部是同一區間的樣本內結果,沒有樣本外驗證;月頻換股的周轉率未列入本文,舊版 117 檔的實際成本會高於表列數字。

投資警語:本文僅供教學參考,不構成投資建議。過去績效不代表未來表現,投資有風險。

最後更新:2026-09|回測區間:2018-01 ~ 2026-08|作者:FinLab 量化研究團隊(經量化研究員審閱)

FinLab AI

想建立自己的策略?

用自然語言描述你的選股想法,AI 自動驗證、回測、給你答案

免費開始

更多多因子選股研究

查看全部