跳至主要內容

四因子複合(品質+動能+低波+營收)

回測視窗 2018-03-31 〜 2026-06-09 · 資料截至 2026-06-09

同時納入品質、動能、低波動與營收成長四個因子,低波動給 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)起算。

淨值曲線(對數座標)

四因子複合(品質+動能+低波+營收) vs 0050 買進持有(含息)
1x2x3x5x2018-03-312019-08-132020-12-232022-05-162023-09-262025-02-192026-06-09
四因子複合(品質+動能+低波+營收)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,
))
到 FinLab Studio 重現這支策略

在 Studio 裡用同一套真實資料調整條件、重跑回測。

本頁策略與回測結果僅供教學與研究用途,詳見服務條款