低波動優化版(風險調整報酬+品質+動能)
純低波動選股在台股長期輸給 0050,改用風險調整報酬因子並疊加 ROE、營收成長與複合動能後,季換前 15 檔。2013 年 9 月至 2026 年 6 月台股回測年化報酬 33.3%、日夏普 1.13、最大回撤 -38.2%;同視窗(2013 年 9 月起)0050 含息年化 20.9%、最大回撤 -34.0%。
投資警語:本文僅供教學參考,不構成投資建議。過去績效不代表未來表現,投資有風險。
回測績效
- 年化報酬率
- 33.3%
- 日夏普
- 1.13
- 最大回撤
- -38.2%
- 總報酬率
- 3765%
- 平均持股
- 14.9 檔
回測含手續費 0.1425% 與賣出證交稅 0.3%(finlab 套件 sim() 預設成本),日夏普未扣無風險利率。策略淨值自資料可得日(2013-09-30)起算。
淨值曲線(對數座標)
低波動優化版(風險調整報酬+品質+動能)0050 買進持有(含息)
與 0050 含息買進持有對照
| 對象 | 年化報酬率 | 日夏普 | 最大回撤 | 統計視窗 |
|---|---|---|---|---|
| 低波動優化版(風險調整報酬+品質+動能) | 33.3% | 1.13 | -38.2% | 2013-09-30 〜 2026-06-18 |
| 0050 買進持有(含息) | 20.9% | 1.14 | -34.0% | 2013-09-30 〜 2026-06-18 |
註:0050 含息若從更早的 2008-01-01 起算(含 2008 年金融海嘯),至 2026-06-20 為年化 14.8%、日夏普 0.74、最大回撤 -52.4%;上表對照採與策略淨值相同的統計視窗。
完整策略程式碼
安裝 finlab 套件後即可直接執行重現(finlab.login() 會自動引導登入,不需手動填 token)。想用 AI 輔助安裝與跑第一次回測,可以走 安裝教學。
展開程式碼(strategy.py)
"""低波動選股策略 — 與 /blog/low-volatility-strategy 同口徑的完整可重現程式。
回測資料自 2008-01-01 取得;本頁策略(風險調整優化版)用到的 ROE 與營收資料較晚才齊備,淨值曲線實際自 2013-09-30 起算、統計至 2026-06-18。執行前安裝 finlab 並登入(finlab 會自動引導):
pip install finlab
python strategy.py
包含:低波動 vs 高波動異象實測,以及風險調整報酬優化版
(風險調整報酬 + ROE + 營收成長 + 複合動能、季換股、前 15)。
"""
import finlab
from finlab import data
from finlab.backtest import sim
finlab.login() # finlab 會自動引導登入
adj = data.get("etl:adj_close")
volume = data.get("price:成交股數")
roe = data.get("fundamental_features:ROE稅後")
yoy = data.get("monthly_revenue:去年同月增減(%)")
ret = adj.pct_change()
vol60 = ret.rolling(60).std() # 60 日波動率
liquid = volume.average(20) > 100 * 1000 # 平均日成交 > 100 張
universe = liquid & adj.notna()
vol_u = vol60[universe]
# 低波動異象:60 日波動率最低 30 檔 vs 最高 30 檔,月換股
pos_low = vol_u.rank(axis=1, ascending=True) <= 30
pos_high = vol_u.rank(axis=1, ascending=False) <= 30
sim(pos_low, resample="M", fee_ratio=0.001425, tax_ratio=0.003).display()
sim(pos_high, resample="M", fee_ratio=0.001425, tax_ratio=0.003).display()
# 風險調整報酬優化版:個股 6M 報酬/波動最高,加品質與複合動能,前 15、季換股
ret6m = adj.pct_change(120)
vol120 = ret.rolling(120).std()
mom_3m = adj.pct_change(60)
risk_adj = ret6m / vol120
pool = universe & (roe > 5) & (yoy > 0) & (mom_3m > 0) & (ret6m > 0)
position = risk_adj[pool].is_largest(15)
report = sim(position, resample="Q", fee_ratio=0.001425, tax_ratio=0.003)
report.display()
print(report.get_stats())
在 Studio 裡用同一套真實資料調整條件、重跑回測。