跳至主要內容

動能輪動(動能+低波動+品質)

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

以動能、低波動與 ROE 品質三個面向打分數,分數平方加權取前 20 檔,每月自動再平衡。2018 年至 2026 年台股回測年化報酬 29.0%、日夏普 1.23、最大回撤 -41.5%;同視窗 0050 含息年化 25.1%、最大回撤 -34.0%。

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

回測績效

年化報酬率
29.0%
日夏普
1.23
最大回撤
-41.5%
總報酬率
715%
平均持股
20.0 檔

回測含手續費 0.1425% 與賣出證交稅 0.3%(finlab 套件 sim() 預設成本),日夏普未扣無風險利率。策略淨值自資料可得日(2018-03-14)起算。

淨值曲線(對數座標)

動能輪動(動能+低波動+品質) vs 0050 買進持有(含息)
1x2x3x5x8x2018-03-142019-07-242020-12-042022-04-262023-09-072025-01-222026-06-09
動能輪動(動能+低波動+品質)0050 買進持有(含息)

與 0050 含息買進持有對照

對象年化報酬率日夏普最大回撤統計視窗
動能輪動(動能+低波動+品質)29.0%1.23-41.5%2018-03-14 〜 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/program-trading 頁面數字來源)
# ----------------------------------------------------------------------------
# 三套策略共用「月頻自動再平衡」的執行框架,差別在選股邏輯:
#   1. 動能輪動(動能+低波動+品質)    ← 頁面主軸
#   2. 均線趨勢過濾(多頭排列+動能)
#   3. 月營收公告自動換股(營收年增+短動能)
# 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 用
rev   = data.get('monthly_revenue:去年同月增減(%)')
roe   = data.get('fundamental_features:ROE稅後').index_str_to_date()  # 對齊公布日,避免前視偏差
mktv  = data.get('etl:market_value')
volume = data.get('price:成交股數')
amount = (close * volume).rolling(60).mean()                         # 近 60 日日均成交額


def monthly(df):
    return df.reindex(close.index, method='ffill').resample('ME').last()


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


# ---- 策略一:動能輪動(動能+低波動+品質,頁面主軸)-------------------------
mom3m = monthly(close.pct_change(60))
mom6m = monthly(close.pct_change(120).shift(20))     # 扣最近 1 月,避免短期反轉
volm  = monthly(close.pct_change().rolling(60).std())
roem  = monthly(roe)
revm  = monthly(rev)
mktvm = monthly(mktv)

pool = (mktvm.rank(axis=1, ascending=False) <= 200) & (roem > 0) & (revm > 0)


def rk(df, asc=True):
    return df.where(pool).rank(axis=1, pct=True, ascending=asc).fillna(0)


score  = rk(mom3m) + rk(mom6m) + rk(volm, asc=False) + rk(roem)
mask   = score.where(pool).rank(axis=1, ascending=False) <= 20
masked = (score.where(mask, 0)) ** 2                  # 分數平方加權,集中在高分股
w1     = masked.div(masked.sum(axis=1).replace(0, np.nan), axis=0).fillna(0)

report_momentum = run(w1, offset='14D', name='動能輪動')

# ---- 策略二:均線趨勢過濾(多頭排列+動能)----------------------------------
ma20, ma60, ma120 = close.rolling(20).mean(), close.rolling(60).mean(), close.rolling(120).mean()
mom6 = close.pct_change(120)
pool_trend = (mktv.rank(axis=1, ascending=False) <= 200) & (amount.rank(axis=1, pct=True) > 0.5)
uptrend = (close > ma120) & (ma20 > ma60)
w2 = mom6.where(pool_trend & uptrend).is_largest(20)

report_trend = run(w2, name='均線趨勢過濾')

# ---- 策略三:月營收公告自動換股 ----------------------------------------------
revd = rev.reindex(close.index, method='ffill')
mom3 = close.pct_change(60)
pool_rev = ((mktv.rank(axis=1, ascending=False) <= 250)
            & (amount.rank(axis=1, pct=True) > 0.5) & (revd > 20))


def rk_rev(df):
    return df.where(pool_rev).rank(axis=1, pct=True).fillna(0)


score3 = rk_rev(revd) + rk_rev(mom3)
mask3  = score3.where(pool_rev).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_revenue = run(w3, offset='14D', 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 裡用同一套真實資料調整條件、重跑回測。

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