
這篇 python 量化交易教學會帶你從 pip install finlab 一路寫到第一個能跑的台股回測。示範用的是一個可下載、可重現的三因子複合策略:品質(ROE)、動能(120 日報酬)、低波動(120 日報酬標準差,負向),每月選 25 檔、反波動加權、加上大盤站上 120 日均線才進場的濾網。整段策略約 20 行,跑完就能得到下面這條權益曲線。
關鍵數字
下表是這個策略的真實回測結果。招牌期間取 2020-01 至 2026-06,同時誠實附上 2013-2026 全期數字(兩者差異在下方「回測方法」段落會說清楚)。
| 指標 | 本策略 2020-2026 | 本策略 全期 2013-2026 | 含息 0050(同期 2020-2026) |
|---|---|---|---|
| 年化報酬 CAGR | 21.2% | 15.8% | 約 30% |
| 夏普比率 Sharpe | 1.58 | 1.28 | 約 1.46 |
| Sortino | 2.46 | 1.97 | — |
| Calmar | 1.59 | — | — |
| 最大回撤 | -13.4% | -13.4% | 深得多 |
| 年化波動 | 11.8% | — | — |
| alpha / beta | 0.118 / 0.335 | — | — |
| 月勝率 | 53.6% | — | — |
兩件事要先講白:第一,raw 報酬上含息 0050 同期 CAGR 約 30%,其實高過本策略的 21.2%,那是台積電權值股超級多頭的結果。第二,本策略的價值在風險調整後:Sharpe 1.58 高於含息 0050 同期約 1.46,而且最大回撤只有 -13.4%,0050 同期回撤深得多。所以本文一律用「以較低回撤、較高 Sharpe 的風險調整後勝出」描述,不會宣稱「報酬贏過大盤」。關於這條口徑,你可以對照我們在 多因子選股能否打敗 0050 裡的同一基準討論。
為什麼用 Python + finlab,而不是自寫 Python 或 MultiCharts
量化交易的第一道門檻不是策略,是資料。台股的調整收盤價、財報公布日對齊、成交金額、全額交割註記、產業分類,這些散落在公開觀測站、交易所、櫃買的資料,自己用 requests 爬、用 pandas 清,光是把前視偏差處理乾淨就能耗掉好幾週。
下面把三條常見路徑攤開比較:
| 路徑 | 台股資料 | 寫程式門檻 | 回測引擎 | 真正的時間成本 |
|---|---|---|---|---|
| 純自寫 Python(requests + pandas) | 自己爬、自己清、自己對齊公布日 | 高 | 自己寫,容易有前視偏差 | 數週起跳,且難驗證 |
| MultiCharts / TradingView | 偏國際商品,台股個股財報資料不齊 | 中(EasyLanguage / Pine) | 內建,但難做橫斷面選股 | 做不出多因子排名選股 |
pip install finlab |
台股價量、財報、籌碼、產業分類一套到位 | 低,Python 一行取資料 | sim() 已內扣手續費與證交稅 |
十分鐘跑出第一個回測 |
MultiCharts 與 TradingView 擅長的是單一商品的進出場訊號,但量化選股的核心是「每月在全市場做橫斷面排名,挑出前段班」,這類運算在 K 線型平台上很難自然表達。finlab 的差異在於它是一個 pip 套件:資料、橫斷面排名、月頻換股、成本內扣都在同一個 DataFrame 流程裡。想更完整地比較工具,可參考 FinLab vs QuantPass 與 FinLab vs TEJ 兩篇評估,以及 程式交易旗艦頁。
如果你還不確定「回測」到底在做什麼,先讀 什麼是回測 補概念,再回來跑程式。
環境安裝:pip install finlab 與登入
整個量化交易程式環境只需要兩步。先安裝套件:
顯示程式碼
pip install finlab接著用你的 API token 登入(到 finlab 會員頁面取得,登入後才能下載資料):
顯示程式碼
import finlab
finlab.login('你的 API TOKEN')登入成功後,所有台股資料都用 data.get('資料表名稱') 取得,回傳的是一個增強版的 pandas DataFrame。取單一張價量表的最小範例如下(這也是 用 Python 取得台股資料 的起點):
顯示程式碼
from finlab import data
close = data.get('price:收盤價') # 全市場每日收盤價,index 是日期、欄位是股票代號
close.tail()到這裡你的量化交易 python 環境就完成了。接下來把三個因子一個一個拼起來。
三因子複合策略逐步講解
這個策略的設計目標放在風險調整後表現,用三個方向互補的因子換取更平滑的權益曲線與更淺的回撤,而非單純衝高 CAGR。三個因子各自做全市場橫斷面百分位排名,再取平均當綜合分數。
因子一:品質(ROE 稅後)
品質因子衡量「公司賺錢的效率」。用稅後 ROE,愈高代表用同樣股東權益賺到愈多錢。關鍵是財報有公布時間差,必須用 index_str_to_date() 把財報對齊到實際公布日,否則會用到當下還拿不到的資料,產生前視偏差。
顯示程式碼
roe = data.get('fundamental_features:ROE稅後').index_str_to_date().reindex(close.index, method='ffill')因子二:動能(120 日報酬)
動能因子捕捉「近半年漲得相對強的股票傾向繼續強」這個橫斷面現象。用 120 個交易日(約半年)的價格比值:
顯示程式碼
momentum = close / close.shift(120)因子三:低波動(120 日報酬標準差,負向)
低波動因子是這個策略壓低回撤的關鍵。用日報酬的 120 日滾動標準差,並加上負號,讓「波動低」的股票排名靠前:
顯示程式碼
low_vol = -close.pct_change().rolling(120).std()三因子合成、選股與加權
三個因子各自做橫斷面百分位排名(rank(pct=True))後平均,得到綜合分數;每月選分數最高的 25 檔;權重用 60 日波動的倒數(反波動加權,波動低者權重高);最後乘上大盤濾網,加權報酬指數站上 120 日均線才進場,否則整體空手抱現金。
顯示程式碼
import finlab
from finlab import data
from finlab.backtest import sim
finlab.login('你的 API TOKEN')
close = data.get('price:收盤價'); volume = data.get('price:成交股數')
amount = (close*volume).average(60) # 60 日均成交金額,做流動性過濾
flagged = data.get('etl:is_flagged_stock').reindex(close.index, method='ffill').fillna(False)
cats = data.get('security_categories').set_index('stock_id')['category']
bad = set(cats[cats.isin(['金融保險','ETF','ETN','存託憑證','受益證券','創新板'])].index)
keep = [c for c in close.columns if c not in bad] # 排除非個股與特殊類別
universe = (amount > 50_000_000) & (close > 10) & (~flagged) # 流動性、低價股、全額交割過濾
roe = data.get('fundamental_features:ROE稅後').index_str_to_date().reindex(close.index, method='ffill')
momentum = close / close.shift(120)
low_vol = -close.pct_change().rolling(120).std()
cols = sorted(set(keep) & set(roe.columns))
rank = lambda df: df[cols].rank(axis=1, pct=True) # 橫斷面百分位排名
score = ((rank(roe)+rank(momentum)+rank(low_vol))/3).where(universe[cols])
selected = score.rank(axis=1, ascending=False) <= 25 # 每月分數最高 25 檔
inv_vol = (1/close.pct_change().rolling(60).std())[cols] # 反波動權重:波動低者權重高
weight = (selected*inv_vol); weight = weight.div(weight.sum(axis=1), axis=0).fillna(0)
twii = data.get('benchmark_return:發行量加權股價報酬指數').iloc[:,0]
market_up = (twii > twii.rolling(120).mean()).reindex(close.index, method='ffill').fillna(False)
weight = weight.mul(market_up.astype(int), axis=0) # 大盤濾網:站上 120MA 才進場
report = sim(weight, resample='M', fee_ratio=1.425/1000/3, name='品質動能低波複合')
report.display()完整可執行腳本可直接下載:strategy.py。把 token 換成你自己的,跑完就會看到下面這張權益曲線。
回測結果
下圖是策略累積淨值對照含息 0050 的曲線。可以看到 2020-2026 期間策略走勢相對平滑,在 2022 年台股回檔時(大盤濾網把部位降到現金)避開了大部分跌幅。

逐年報酬對照如下。策略並非每一年都領先,但在下跌年的虧損明顯較小,這正是低波動因子加大盤濾網的設計目的。

招牌期間的完整指標:Sharpe 1.58、Sortino 2.46、Calmar 1.59、CAGR 21.2%、年化波動 11.8%、alpha 0.118、beta 0.335、月勝率 53.6%。beta 只有 0.335,代表策略對大盤的敏感度低,這也是它在風險調整後能勝出的結構性原因。
風險與回撤
回撤(drawdown)比 CAGR 更能反映你實際抱得住抱不住。下圖是策略的回撤曲線,最大回撤 -13.4%,出現在波動最劇烈的區間。

-13.4% 的最大回撤,搭配 21.2% 的 CAGR,得到 Calmar 1.59(報酬除以最大回撤)。含息 0050 同期回撤深得多,這是兩者風險輪廓最直觀的差距。需要提醒的是:過去回撤淺不保證未來不會更深,任何單一因子失效或市場結構改變,都可能讓回撤超出歷史區間。
回測方法(嚴謹度揭露)
漂亮的數字若不交代怎麼算出來,就只是廣告。以下逐項說明這份回測的設定,有做寫數值、沒做誠實標明:
- 交易成本: finlab 的
sim()預設已內扣手續費與證交稅。程式裡fee_ratio=1.425/1000/3代表手續費以 0.1425% 打三折計算(約 0.0475%);賣出證交稅 0.3% 由sim()自動扣除。 - 滑價: 本文未額外假設滑價。月頻換股、單檔權重分散,衝擊相對可控,但實際衝擊取決於資金規模與當下成交量,本文未估算到滑價層級。
- 股票池: 全上市櫃個股,經下列流動性與類別過濾後的集合。
- 流動性過濾: 60 日均成交金額 > 5,000 萬元、股價 > 10 元,確保回測買得到的股票實單也買得到。
- 排除類別: 排除全額交割股(
etl:is_flagged_stock)、金融保險、ETF、ETN、存託憑證、受益證券、創新板。排除全額交割與特殊類別是為了降低生存者偏誤與不可交易的標的。 - 前視偏差: 財報(ROE)用
index_str_to_date()對齊實際公布日,再ffill到每日,確保每一天只用到當天真正已公布的財報。 - 權重: 反波動加權(60 日波動倒數),非等權;權重在 25 檔之間正規化加總為 1。本文未設單檔上限,反波動加權本身會壓低個股集中度。
- 周轉率: 月頻換股(
resample='M')。本文未逐期估算 turnover 數值,標明未估。 - 樣本內外: 本文為全段 in-sample,未做嚴格的樣本外保留期測試;改以下一節的參數網格作為穩健性佐證。
- 全期 vs 招牌期間誠實揭露: 招牌數字取 2020-2026(Sharpe 1.58、CAGR 21.2%);全期 2013-2026 為 Sharpe 1.28、Sortino 1.97、CAGR 15.8%、最大回撤同為 -13.4%。兩者差異主要來自 2020 後的台股多頭環境,讀者應以全期數字作為較保守的預期基準。
關於「回測看起來很好、實單卻失靈」的系統性風險,量化投資的挑戰與過擬合 有更完整的討論,建議搭配閱讀。
參數穩健性
單一組參數跑出 Sharpe 1.58 不能說明問題;真正該問的是:換個持股數、換個大盤濾網長度,結論會不會崩掉?下圖是 2020-2026 的參數網格結果。

持股 20 / 25 / 30 / 35 檔,搭配大盤濾網 100MA 或 120MA,Sharpe 全部落在 1.46 到 1.58 之間,代表結論對「選幾檔、用多長均線」並不敏感。唯一明顯下滑的是把濾網拉長到 150MA,Sharpe 會掉到約 1.2,因為較長均線反應太慢、退場太晚。所以 100MA 到 120MA 是這個策略的穩健區間,本文取 120MA 是其中偏保守的一端。
把單因子分開檢視也是嚴謹的一環:低波動是壓低回撤的關鍵因子,品質與動能提供報酬來源。三者合成的目的就是讓任一因子失效時,其他兩者仍能撐住整體。
誠實基準對照:風險調整後勝出
這一節必須講清楚,以免被當成又一篇浮誇的策略文。
含息 0050 在 2020-2026 累積約 5.45 倍、CAGR 約 30%,raw 報酬其實高過本策略的 21.2%。原因很單純:這段期間台積電等權值股走超級多頭,被動持有 0050 等於重壓贏家。所以就純報酬而言,本策略並沒有贏過含息 0050。
本策略的價值在另一個維度:以較低回撤、較高 Sharpe 的風險調整後勝出。同期 Sharpe 1.58 高於含息 0050 約 1.46,且最大回撤只有 -13.4%,0050 同期回撤深得多。換句話說,它用接近的長期報酬,換到更平穩的曲線與更小的最大虧損。對於抱不住大跌、會在低點停損的投資人,這個風險輪廓的實際意義可能比 raw CAGR 更重要。這也呼應 夏普比率 衡量「每承擔一單位風險換到多少報酬」的核心精神。
新手下一步
如果這是你的第一個量化交易程式,建議照這個順序往下走:
- 先把上面的 strategy.py 跑通,確認自己的環境與 token 沒問題。
- 一次只改一個因子或一個參數,觀察 Sharpe 與回撤怎麼變,建立對因子的直覺。
- 讀 FinLab 量化平台新手指南 了解更多資料表與
sim()參數。 - 把選股邏輯換成你自己的想法,可參考 台股選股 5 步驟 與 如何在台股選股。
- 想理解因子有沒有效,讀 資訊係數 IC 是什麼;想了解 AI 在量化研究的角色,讀 AI 量化研究。
- 進階階段務必留意風險面:量化交易的缺點與風險、量化交易的職涯與薪資,以及若你考慮券商生態,可看 富途牛牛量化交易在台灣。
所有名詞若有不熟,都可以到 詞彙表 查核心定義後再回來。
FAQ
Q1:完全不會寫程式,能做 python 量化交易嗎? 可以從很小的步子開始。本文的策略整段才約 20 行,你不需要先學完 Python,只要能把程式碼貼到環境裡、把 token 換成自己的、按下執行。先「跑得起來」再「看得懂」,接著一次改一個參數,是新手最快的學習路徑。
Q2:量化交易程式需要多少錢才能開始?
軟體層面,pip install finlab 本身免費,Python 也免費。資料下載需要 finlab 帳號,有免費試用可先跑回測驗證流程。真正要花錢的是實際投入市場的本金,以及換股產生的手續費與證交稅(本文回測已把這兩項內扣)。
Q3:量化交易 python 和一般技術分析程式有什麼不同?
技術分析程式多半針對單一商品判斷進出場訊號(例如均線交叉)。量化選股則是每月在全市場做橫斷面排名,挑出相對最好的一批股票。本文的三因子複合屬於後者,核心運算是 rank(axis=1) 的橫斷面排名,而非單檔的 K 線訊號。
Q4:回測 Sharpe 1.58 準嗎?實單能複製嗎? 回測是「在歷史資料上的模擬」,準的前提是方法乾淨:本文已處理前視偏差(財報對齊公布日)、流動性過濾、排除不可交易類別、內扣成本。即便如此,實單仍會遇到滑價、資金容量、策略隨時間衰退等回測難以完全反映的因素。把全期 2013-2026 的 Sharpe 1.28 當較保守的預期會更務實。
Q5:這個策略的容量(資金上限)大概多少? 依本文的流動性過濾與持股檔數估算,策略容量約 NT$6,200 萬 / 次再平衡。超過這個規模,換股時的市場衝擊會明顯侵蝕報酬,需要放寬持股檔數或調整流動性門檻。
Q6:為什麼不直接買含息 0050 就好? 就 2020-2026 的純報酬而言,含息 0050 的 CAGR 約 30% 確實高過本策略的 21.2%。本策略的訴求是風險調整後:Sharpe 1.58 高於 0050 約 1.46、最大回撤只有 -13.4%。適不適合你,取決於你更在意 raw 報酬,還是更在意曲線平穩與回撤控制。
Q7:多久要重新換股?換太頻繁會被成本吃掉嗎?
本策略月頻換股(resample='M')。月頻在台股是成本與訊號新鮮度之間相對平衡的選擇,回測已內扣手續費與證交稅。若改成週頻,turnover 與成本都會上升,需要重新驗證淨報酬是否還划算。
Q8:可以把這套策略套用到我自己挑的因子嗎?
可以,而且這正是建議的進階方向。把 roe、momentum、low_vol 任一個換成你自己的因子(例如營收成長、籌碼指標),其餘合成、選股、加權、濾網的框架不變。換因子後務必重跑回測與參數網格,確認新組合在風險調整後仍站得住。
投資有風險,過去績效不代表未來表現,本文僅供教學參考,不構成投資建議。回測數字會隨資料更新而變動。
最後更新:2026-06|回測區間:2020-01~2026-06(另列全期 2013-2026)|作者:FinLab 量化研究團隊(經量化研究員審閱)
參考文獻
- Jegadeesh, N. & Titman, S. (1993). 動能效應的開創性實證:過去 3 到 12 個月相對強勢的股票,在接下來數月傾向繼續跑贏,支持本文以 120 日報酬作為動能因子。
- Ang, A., Hodrick, R., Xing, Y. & Zhang, X. (2006). 低波動異常:波動較低的股票長期風險調整後報酬反而較好,對應本文低波動因子(120 日報酬標準差負向)與反波動加權的設計動機。
- Sharpe, W. F. (1966, 1994). 夏普比率:用「每承擔一單位波動換到多少超額報酬」衡量策略,正是本文以 Sharpe 1.58 而非 raw CAGR 主張勝出的依據。
- Bailey, D., Borwein, J., López de Prado, M. & Zhu, Q. (2014). 回測過擬合:在多組設定中挑出最漂亮的一組會高估真實績效,提醒讀者重視本文同時揭露全期 2013-2026(Sharpe 1.28)與參數網格穩健性,而非只看招牌期間的 1.58。
FinLab AI
想建立自己的策略?
用自然語言描述你的選股想法,AI 自動驗證、回測、給你答案
免費開始