四因子複合(品質+動能+低波+營收)
同時納入品質、動能、低波動與營收成長四個因子,低波動給 2 倍權重、分數三次方加權取前 25 檔。2018 年至 2026 年台股回測年化報酬 20.1%、日夏普 1.46、最大回撤 -24.8%,回撤明顯低於 2018 年以來 0050 含息的 -34.0%,但年化報酬低於 0050 同期的 25.1%。
投資警語:本文僅供教學參考,不構成投資建議。過去績效不代表未來表現,投資有風險。
回測績效
- 年化報酬率
- 20.1%
- 日夏普
- 1.46
- 最大回撤
- -24.8%
- 總報酬率
- 351%
- 平均持股
- 25.0 檔
回測含手續費 0.1425% 與賣出證交稅 0.3%(finlab 套件 sim() 預設成本),日夏普未扣無風險利率。策略淨值自資料可得日(2018-03-31)起算。
淨值曲線(對數座標)
四因子複合(品質+動能+低波+營收)0050 買進持有(含息)
與 0050 含息買進持有對照
| 對象 | 年化報酬率 | 日夏普 | 最大回撤 | 統計視窗 |
|---|---|---|---|---|
| 四因子複合(品質+動能+低波+營收) | 20.1% | 1.46 | -24.8% | 2018-03-31 〜 2026-06-09 |
| 0050 買進持有(含息) | 25.1% | 1.22 | -34.0% | 2018-01-01 〜 2026-06-09 |
完整策略程式碼
安裝 finlab 套件後即可直接執行重現(finlab.login() 會自動引導登入,不需手動填 token)。想用 AI 輔助安裝與跑第一次回測,可以走 安裝教學。
展開程式碼(strategy.py)
# ============================================================================
# 股票選股三策略回測(finlab.finance/tools/stock-selection 頁面數字來源)
# ----------------------------------------------------------------------------
# 三套策略各代表一個因子家族:
# 1. 四因子複合(品質+動能+低波(2×)+營收,分數三次方加權前 25) ← 頁面主軸
# 2. 價值+品質+股息(低 PB+高 ROE+高殖利率,動能>0 確認,季換前 20)
# 3. 籌碼選股(投信 20 日買超強度+動能,月換前 15)
# 單因子與敏感度實驗另見 factor-comparison.py / robustness.py。
# Benchmark:0050 含息(etl:adj_close,純算術買進持有,不經 sim、不含成本)。
# 執行:安裝 finlab 後直接跑(finlab.login() 會自動引導登入)。
# 回測區間:2018-01-01 ~ 2026-06-09;sim() 已內扣手續費 0.1425% 與證交稅 0.3%。
# ============================================================================
import numpy as np
from finlab import data
from finlab.backtest import sim
START, END = '2018-01-01', '2026-06-09'
# ---- 載入資料 ---------------------------------------------------------------
close = data.get('price:收盤價')
adj = data.get('etl:adj_close') # 含息,benchmark 用
pb = data.get('price_earning_ratio:股價淨值比')
dy = data.get('price_earning_ratio:殖利率(%)')
roe = data.get('fundamental_features:ROE稅後').index_str_to_date().reindex(close.index, method='ffill')
rev = data.get('monthly_revenue:去年同月增減(%)').reindex(close.index, method='ffill')
trust = data.get('institutional_investors_trading_summary:投信買賣超股數')
volume = data.get('price:成交股數')
amount = (close * volume).rolling(60).mean() # 近 60 日日均成交額
liquid = amount.rank(axis=1, pct=True) > 0.5 # 流動性過濾:成交額前 50%
def run(position, resample='M', offset=None, name=''):
kwargs = dict(resample=resample, fee_ratio=0.001425, tax_ratio=0.003,
upload=False, name=name)
if offset:
kwargs['resample_offset'] = offset
report = sim(position[position.index >= START], **kwargs)
print(name, report.get_stats())
return report
# ---- 策略一:四因子複合(品質+動能+低波(2×)+營收,頁面主軸)----------------
quality = roe.rank(axis=1, pct=True)
momentum = (close / close.shift(120) - 1).rank(axis=1, pct=True)
lowvol = (-close.pct_change().rolling(120).std()).rank(axis=1, pct=True)
revenue = rev.rank(axis=1, pct=True)
score1 = (quality + momentum + 2 * lowvol + revenue) / 5
score1 = score1.where(liquid)
sel1 = score1.rank(axis=1, ascending=False) <= 25
w1 = (score1 ** 3).where(sel1) # 分數三次方加權,集中在高分股
w1 = w1.div(w1.sum(axis=1), axis=0).fillna(0)
report_composite = run(w1, offset='14D', name='四因子複合')
# ---- 策略二:價值+品質+股息(動能>0 確認,季換)-----------------------------
mom6 = close / close.shift(120) - 1
pool2 = liquid & (pb > 0) & (mom6 > 0)
def rk2(df):
return df.where(pool2).rank(axis=1, pct=True).fillna(0)
score2 = rk2(-pb) + rk2(roe) + rk2(dy)
mask2 = score2.where(pool2).rank(axis=1, ascending=False) <= 20
w2 = score2.where(mask2, 0)
w2 = w2.div(w2.sum(axis=1).replace(0, np.nan), axis=0).fillna(0)
report_value = run(w2, resample='Q', name='價值+品質+股息')
# ---- 策略三:籌碼選股(投信 20 日買超強度+動能)------------------------------
mktv = data.get('etl:market_value')
trust_daily = trust.reindex(close.index).fillna(0)
strength = trust_daily.rolling(20).sum() / volume.rolling(20).mean().replace(0, np.nan)
pool3 = liquid & (mktv.rank(axis=1, ascending=False) <= 600) & (strength > 0)
mom3 = close.pct_change(60)
score3 = (strength.where(pool3).rank(axis=1, pct=True).fillna(0)
+ mom3.where(pool3).rank(axis=1, pct=True).fillna(0))
mask3 = score3.where(pool3).rank(axis=1, ascending=False) <= 15
w3 = score3.where(mask3, 0)
w3 = w3.div(w3.sum(axis=1).replace(0, np.nan), axis=0).fillna(0)
report_chips = run(w3, name='籌碼選股')
# ---- Benchmark:0050 含息(純算術買進持有)----------------------------------
px = adj['0050'].dropna().loc[START:END]
curve = (1 + px.pct_change().fillna(0)).cumprod()
years = (curve.index[-1] - curve.index[0]).days / 365.25
print('0050 含息 buy & hold:CAGR %.2f%%,總報酬 %.1f%%,最大回撤 %.2f%%' % (
(curve.iloc[-1] ** (1 / years) - 1) * 100,
(curve.iloc[-1] - 1) * 100,
(curve / curve.cummax() - 1).min() * 100,
))
在 Studio 裡用同一套真實資料調整條件、重跑回測。