動能輪動(動能+低波動+品質)
以動能、低波動與 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)起算。
淨值曲線(對數座標)
動能輪動(動能+低波動+品質)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,
))
在 Studio 裡用同一套真實資料調整條件、重跑回測。