PEG+營收動能(前 10 檔、季換股)
PEG 介於 0 到 1.5、營業利益成長率大於 5%、營收年增大於 10% 且創 12 個月新高,再用 RSI 與 3 個月股價動能確認趨勢,取 PEG 最低的 10 檔,每季換股。2015 年至 2026 年台股回測年化 38.0%、日夏普 1.32、最大回撤 -35.2%;同視窗 0050 含息年化 20.7%、日夏普 1.08、最大回撤 -34.0%。
投資警語:本文僅供教學參考,不構成投資建議。過去績效不代表未來表現,投資有風險。
回測績效
- 年化報酬率
- 38.0%
- 日夏普
- 1.32
- 最大回撤
- -35.2%
- 總報酬率
- 4059%
- 平均持股
- 9.6 檔
回測含手續費 0.1425% 與賣出證交稅 0.3%(finlab 套件 sim() 預設成本),日夏普未扣無風險利率。策略淨值自資料可得日(2015-03-31)起算。
淨值曲線(對數座標)
PEG+營收動能(前 10 檔、季換股)0050 買進持有(含息)
與 0050 買進持有(含息)對照
| 對象 | 年化報酬率 | 日夏普 | 最大回撤 | 統計視窗 |
|---|---|---|---|---|
| PEG+營收動能(前 10 檔、季換股) | 38.0% | 1.32 | -35.2% | 2015-03-31 〜 2026-10-29 |
| 0050 買進持有(含息) | 20.7% | 1.08 | -34.0% | 2015-03-31 〜 2026-10-29 |
完整策略程式碼
安裝 finlab 套件後即可直接執行重現(finlab.login() 會自動引導登入,不需手動填 token)。想用 AI 輔助安裝與跑第一次回測,可以走安裝教學。
展開程式碼(strategy.py)
"""PEG+營收動能(PEG 0~1.5、前 10 檔、季換股):跑到最新資料的版本(/strategies/pe-pb-peg-backtest 的數字來源)。
/blog/pe-pb-peg-backtest VIP 區「PEG + 營收動能」的程式,條件逐行相同:
- PEG(本益比 ÷ 營業利益成長率)介於 0 到 PEG_MAX、營業利益成長率大於 5%
- 營收年增大於 10%、近 3 個月平均營收創 12 個月新高
- RSI(14) 大於 50、本益比介於 5 到 30、近 60 個交易日股價漲幅為正、20 日均量大於 300 張
- 符合條件的股票裡取 PEG 最低的 TOP_N 檔,等權持有,2015 年起每季換股
- sim() 預設手續費 0.1425% 與證交稅 0.3%;RSI 用 data.indicator,需要 TA-Lib
統計口徑:日夏普 = 日報酬平均 / 標準差 × √252,不扣無風險利率;
0050 含息 = etl:adj_close 還原價買進持有,與策略同一段期間。
執行後會在目前目錄輸出 data.csv(淨值曲線)與 metrics.json(統計)。
pip install finlab TA-Lib
python strategy.py
finlab 需要資料時會自動引導登入,不需要在程式裡填 token。
"""
import json
import pandas as pd
import finlab
from finlab import data
from finlab.backtest import sim
STRATEGY_NAME = "PEG+營收動能(前 10 檔、季換股)"
PEG_MAX = 1.5
TOP_N = 10
def build_report():
pe = data.get("price_earning_ratio:本益比")
growth = data.get("fundamental_features:營業利益成長率")
rev = data.get("monthly_revenue:當月營收")
rev_yoy = data.get("monthly_revenue:去年同月增減(%)")
close = data.get("price:收盤價")
rsi = data.indicator("RSI", timeperiod=14)
volume = data.get("price:成交股數")
peg = pe / growth
rev_ma3 = rev.average(3)
rev_new_high = rev_ma3 == rev_ma3.rolling(12).max()
momentum_3m = close / close.shift(60) - 1
cond = (
(peg > 0) & (peg < PEG_MAX)
& (growth > 5)
& (rev_yoy > 10)
& rev_new_high
& (rsi > 50)
& (pe > 5) & (pe < 30)
& (momentum_3m > 0)
& (volume.average(20) > 300 * 1000)
)
position = peg[cond]
position = position[position > 0].is_smallest(TOP_N)
position = position.reindex(rev.index_str_to_date().index, method="ffill")
return sim(position.loc["2015":], resample="Q", upload=False, name=STRATEGY_NAME)
def curve_stats(curve: pd.Series) -> dict:
curve = curve / curve.iloc[0]
daily = curve.pct_change().dropna()
years = (curve.index[-1] - curve.index[0]).days / 365.25
return {
"cagr": curve.iloc[-1] ** (1 / years) - 1,
"daily_sharpe": daily.mean() / daily.std() * 252 ** 0.5,
"max_drawdown": (curve / curve.cummax() - 1).min(),
"total_return": curve.iloc[-1] - 1,
}
def main() -> None:
finlab.login()
bench_all = data.get("etl:adj_close")["0050"].dropna()
report = build_report()
# 從第一次有部位的那天起算,0050 含息用同一段期間
creturn = report.creturn.dropna()
first_move = (creturn != creturn.iloc[0]).argmax()
creturn = creturn.iloc[max(first_move - 1, 0):]
bench = bench_all.loc[creturn.index[0]:creturn.index[-1]]
strategy_stats = curve_stats(creturn)
bench_stats = curve_stats(bench)
holdings = (report.position.fillna(0) != 0).sum(axis=1)
strategy_stats["avg_holdings"] = holdings[holdings > 0].mean()
start, end = creturn.index[0].date(), creturn.index[-1].date()
print(f"期間 {start} 至 {end}")
for label, s in [("策略", strategy_stats), ("0050 含息", bench_stats)]:
print(f"{label}:年化 {s['cagr']:.2%},日夏普 {s['daily_sharpe']:.2f},最大回撤 {s['max_drawdown']:.2%}")
curves = pd.DataFrame({STRATEGY_NAME: creturn / creturn.iloc[0], "0050含息": bench / bench.iloc[0]})
curves.index.name = "date"
curves.round(6).to_csv("data.csv")
metrics = {
"window": f"{start}/{end}",
"star": STRATEGY_NAME,
"variants": {STRATEGY_NAME: {k: round(float(v), 6) for k, v in strategy_stats.items()}},
"benchmark_0050": {k: round(float(v), 6) for k, v in bench_stats.items()},
"run_date": str(pd.Timestamp.today().date()),
"finlab_version": finlab.__version__,
"generated_by": "static/blog/pe-pb-peg-backtest/validation/strategy.py",
}
with open("metrics.json", "w", encoding="utf-8") as f:
json.dump(metrics, f, ensure_ascii=False, indent=2)
if __name__ == "__main__":
main()
在 Studio 裡用同一套真實資料調整條件、重跑回測。