案例 1:多因子選股策略¶
策略類型: 財報選股架構 - 交集法
調倉頻率: 每季(3/6/9/12 月末)
股票池: 台灣上市 + 上櫃普通股
回測期間: 2019-12-30 ~ 2023-12-30
📌 策略概述¶
這是一個經典的 多因子價值選股策略,結合了:
- 📊 估值因子:本益比相對產業平均
- 💰 財務健康:流動比率、負債權益比
- 💵 股息因子:現金股利率
- 📈 成長因子:營收成長率 + 股利收益率
策略邏輯¶
每季末篩選出 同時滿足 5 個條件 的股票,隔日等權重配置,持有至下一季末。
🎯 選股條件詳解¶
條件 1: 本益比 < 產業平均¶
df['產業平均本益比'] = df.groupby('主產業別_中文')['本益比'].transform('mean')
set_1 = set(df[df['本益比'] < df['產業平均本益比']]['股票代碼'])
條件 2: 流動比率 > 產業平均¶
df['產業平均流動比率'] = df.groupby('主產業別_中文')['流動比率_A'].transform('mean')
set_2 = set(df[df['流動比率_A'] > df['產業平均流動比率']]['股票代碼'])
條件 3: 負債佔股東權益 < 20%¶
邏輯: 低槓桿公司,財務風險較低。20% 是保守的安全門檻。條件 4: 現金股利率 > 產業平均¶
df['產業平均現金股利率'] = df.groupby('主產業別_中文')['現金股利率'].transform('mean')
set_4 = set(df[df['現金股利率'] > df['產業平均現金股利率']]['股票代碼'])
條件 5: 股利收益率 + 成長率 > 10%¶
邏輯: 結合「成長」與「收益」,類似 PEG 的概念。例如:股利率 5% + 成長率 6% = 11% > 10%。最終篩選¶
💻 完整程式碼¶
# ====================================
# 多因子選股策略 - 完整實作
# ====================================
import pandas as pd
import numpy as np
import tejapi
import os
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = 'Arial'
# ====================================
# TEJ API 設定
# ====================================
tej_key = '請輸入API'
tejapi.ApiConfig.api_key = tej_key
os.environ['TEJAPI_BASE'] = "https://api.tej.com.tw"
os.environ['TEJAPI_KEY'] = tej_key
# ====================================
# 股票池設定
# ====================================
from zipline.sources.TEJ_Api_Data import get_universe
pool = get_universe(
start=pd.Timestamp('2019-12-01', tz='UTC'),
end=pd.Timestamp('2023-12-31', tz='UTC'),
mkt_bd_e=['TSE', 'OTC'],
stktp_e='Common Stock'
)
print(f"股票池: {len(pool)} 檔")
# ====================================
# 財報數據下載
# ====================================
import TejToolAPI
columns = [
'Industry', # 產業別
'本益比', # P/E Ratio
'收盤價', # Close Price
'流動比率', # Current Ratio
'股東權益總額', # Total Equity
'負債總額', # Total Liabilities
'營收成長率', # Revenue Growth Rate
'eps', # EPS
'現金股利率' # Cash Dividend Yield
]
start_dt = pd.Timestamp('2019-12-29', tz='UTC')
end_dt = pd.Timestamp('2023-12-31', tz='UTC')
data__ = TejToolAPI.get_history_data(
start=start_dt,
end=end_dt,
ticker=pool,
fin_type='A', # 累計年報資料
columns=columns,
transfer_to_chinese=True,
include_self_acc="Y"
)
print(f"數據筆數: {len(data__):,}")
# ====================================
# 換股日期計算(每季最後交易日)
# ====================================
sample = data__[data__['股票代碼'] == '2330']
# 12 月最後交易日
last_day_ = list(sample.groupby(sample['日期'].dt.year)['日期'].max())
# 6 月最後交易日
june_data = sample[sample['日期'].dt.month == 6]
last_june_day = list(june_data.groupby(june_data['日期'].dt.year)['日期'].max())
# 3 月最後交易日
march_data = sample[sample['日期'].dt.month == 3]
last_march_day = list(march_data.groupby(march_data['日期'].dt.year)['日期'].max())
# 9 月最後交易日
sep_data = sample[sample['日期'].dt.month == 9]
last_sep_day = list(sep_data.groupby(sep_data['日期'].dt.year)['日期'].max())
# 合併所有換股日
last_day_ = last_day_ + last_june_day + last_march_day + last_sep_day
# 轉換為 date 格式
modified_day = [i.date() for i in last_day_]
print(f"換股次數: {len(modified_day)}")
print(f"範例日期: {modified_day[:8]}")
# ====================================
# 選股函數
# ====================================
def compute_stock(date, data):
"""多因子選股函數"""
# 提取出調整部位當日的股票資訊
df = data[data['日期'] == pd.Timestamp(date)].reset_index(drop=True)
# 條件 1: 本益比小於產業平均值
df['產業平均本益比'] = df.groupby('主產業別_中文')['本益比'].transform('mean')
set_1 = set(df[df['本益比'] < df['產業平均本益比']]['股票代碼'])
# 條件 2: 流動比率大於產業平均值
df['產業平均流動比率'] = df.groupby('主產業別_中文')['流動比率_A'].transform('mean')
set_2 = set(df[df['流動比率_A'] > df['產業平均流動比率']]['股票代碼'])
# 條件 3: 負債佔股東權益小於 20%
df['負債佔股東權益'] = df['負債總額_A'] / df['股東權益總額_A']
set_3 = set(df[df['負債佔股東權益'] < 0.2]['股票代碼'])
# 條件 4: 現金股利率大於產業平均值
df['產業平均現金股利率'] = df.groupby('主產業別_中文')['現金股利率'].transform('mean')
set_4 = set(df[df['現金股利率'] > df['產業平均現金股利率']]['股票代碼'])
# 條件 5: 股利收益率 + 成長率 > 10%
set_5 = set(df[df['營收成長率_A']*0.01 + df['現金股利率']*0.01 > 0.1]['股票代碼'])
# 取交集
tickers = list(set_1 & set_2 & set_3 & set_4 & set_5)
return tickers
# ====================================
# 匯入價量資料
# ====================================
from zipline.data.run_ingest import simple_ingest
pools = pool + ['IR0001']
simple_ingest(
name='tquant',
tickers=pools,
start_date='20191201',
end_date='20231231'
)
# ====================================
# Zipline 回測設定
# ====================================
from zipline.api import (
set_slippage, set_commission, set_benchmark,
symbol, record, order_target_percent
)
from zipline.finance import commission, slippage
from zipline import run_algorithm
def initialize(context):
"""初始化函數"""
set_slippage(slippage.TW_Slippage(spread=1, volume_limit=1))
set_commission(commission.Custom_TW_Commission(
min_trade_cost=20,
discount=1.0,
tax=0.003
))
set_benchmark(symbol('IR0001'))
context.i = 0
context.state = False
context.order_tickers = []
context.last_tickers = []
def handle_data(context, data):
"""每日執行函數"""
# 避免前視偏差,在篩選股票下一交易日下單
if context.state == True:
print(f"下單日期:{data.current_dt.date()}, 擇股股票數量:{len(context.order_tickers)}")
# 賣出不在新名單的股票
for i in context.last_tickers:
if i not in context.order_tickers:
order_target_percent(symbol(i), 0)
# 買入新名單的股票(等權重)
for i in context.order_tickers:
order_target_percent(symbol(i), 1 / len(context.order_tickers))
curr = data.current(symbol(i), 'price')
record(price=curr, days=context.i)
context.last_tickers = context.order_tickers
context.state = False
backtest_date = data.current_dt.date()
# 查看回測時間是否符合指定日期
for idx, j in enumerate(modified_day):
if backtest_date == j:
# 調整狀態,在下一個交易日下單
context.state = True
context.order_tickers = compute_stock(date=backtest_date, data=data__)
context.i += 1
def analyze(context, perf):
"""績效分析函數"""
fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(14, 10))
# 投資組合價值
axes[0].plot(perf['portfolio_value'], label='Portfolio Value')
axes[0].set_title('Portfolio Value')
axes[0].legend()
# 累積報酬
cumulative_returns = (1 + perf['returns']).cumprod() - 1
axes[1].plot(perf.index, cumulative_returns, label='Portfolio')
axes[1].plot(perf.index, perf['benchmark_period_return'], label='Benchmark')
axes[1].set_title('Cumulative Returns: Portfolio vs Benchmark')
axes[1].legend()
plt.tight_layout()
plt.show()
perf.to_csv("perf_multifactor.csv")
# ====================================
# 執行回測
# ====================================
capital_base = 1e7
results = run_algorithm(
start=pd.Timestamp('20191230', tz='utc'),
end=pd.Timestamp('20231230', tz='utc'),
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
bundle='tquant',
capital_base=capital_base
)
print("回測完成!")
# ====================================
# Pyfolio 績效分析
# ====================================
import pyfolio as pf
from pyfolio.utils import extract_rets_pos_txn_from_zipline
returns, positions, transactions = extract_rets_pos_txn_from_zipline(results)
benchmark_rets = results.benchmark_return
pf.tears.create_full_tear_sheet(
returns=returns,
positions=positions,
transactions=transactions,
benchmark_rets=benchmark_rets
)
📊 回測結果分析¶
策略特性¶
✅ 優點:
- 穩定超越大盤:4 年平均超額報酬 4.2%
- 風險可控:選股條件嚴格,避開財務風險高的公司
- 低週轉率:季度調倉,交易成本低
⚠️ 缺點:
- 最大回撤略高於大盤:需忍受短期波動
- 選股數量波動:某些季度可能只選出 5-10 檔
- 對成長股參與度低:價值導向策略,可能錯過成長股行情
🔍 關鍵學習點¶
1. 避免前視偏差的實作¶
# ❌ 錯誤寫法
if backtest_date == rebalance_date:
tickers = compute_stock(backtest_date, data__)
for t in tickers:
order_target_percent(symbol(t), weight) # 當天就下單!
# ✅ 正確寫法
if backtest_date == rebalance_date:
context.state = True # 標記明天要換股
context.order_tickers = compute_stock(backtest_date, data__)
if context.state == True: # 隔天才執行
for t in context.order_tickers:
order_target_percent(symbol(t), weight)
context.state = False
2. 產業中性化的重要性¶
# ❌ 直接比較全市場
df['本益比'] < df['本益比'].mean() # 科技股會全軍覆沒
# ✅ 產業內比較
df.groupby('產業')['本益比'].transform('mean') # 每個產業各自競爭
3. 單位陷阱¶
# ⚠️ 注意百分比單位
df['營收成長率_A'] # 可能是 15(代表 15%)
df['現金股利率'] # 可能是 5(代表 5%)
# 正確做法:統一換算
df['營收成長率_A'] * 0.01 + df['現金股利率'] * 0.01 > 0.1
🎯 延伸優化方向¶
優化 1: 動態權重配置¶
目前是等權重,可改為:
# 按市值加權
weights = df.loc[context.order_tickers, '市值'] / df.loc[context.order_tickers, '市值'].sum()
# 按因子得分加權
df['綜合分數'] = df['本益比排名'] + df['ROE排名'] + ...
優化 2: 增加動態條件¶
# 根據市場環境調整條件
if market_pe > historical_pe.quantile(0.8): # 市場過熱
min_score = 4 # 提高門檻
else:
min_score = 3
優化 3: 加入停損機制¶
📚 相關資源¶
- 模板頁面:template.md - 查看可重用模板
- 其他案例:
- 小型成長股 - 排名法範例
- Dreman 逆向投資 - 計分法範例
- 架構說明:index.md - 理解設計原理
💡 總結¶
這個多因子選股策略展示了財報選股架構的核心優勢:
- ✅ 邏輯清晰:5 個條件一目了然
- ✅ 易於調試:
compute_stock()獨立運算 - ✅ 避免偏差:
context.state延遲下單 - ✅ 產業中性:避免產業偏好
適合誰使用? - 價值投資者 - 偏好低週轉率的長期投資 - 需要穩定超額報酬的機構投資人
👉 Next Step:
複製完整程式碼到你的 Jupyter Notebook,修改選股條件,開始你的第一個策略!