问题描述
解决方案
这是一个基于 Fama-French 三因子模型 的选股策略实现。
策略原理
Fama-French 三因子模型认为股票的超额收益主要来源于三个因素:
- 市场风险因子 (MKT):市场整体的超额回报。
- 市值因子 (SMB - Small Minus Big):小市值股票通常优于大市值股票。
- 账面市值比因子 (HML - High Minus Low):高账面市值比(即低市净率 PB)的股票通常优于低账面市值比的股票。
策略逻辑
本策略将构建一个利用 SMB 和 HML 因子的多头组合:
- 股票池:选取中证500指数成分股(
000905.XBHS),因为相比沪深300,中证500更能体现中小市值特征。 - 过滤:剔除 ST、停牌、退市股票。
- 打分排序:
- 市值 (Market Cap):从小到大排序(因子:小市值)。
- 市净率 (PB):从小到大排序(因子:高价值)。
- 综合得分:将两者的排名相加,得分越低越好。
- 调仓频率:按月调仓(每月第一个交易日)。
- 资金分配:等权重买入排名前 N 的股票。
PTrade 策略代码
import pandas as pd
import numpy as np
def initialize(context):
"""
策略初始化函数
"""
# 设置基准为中证500
set_benchmark('000905.XBHS')
# 设置股票池为中证500(更能体现中小市值因子)
g.index_code = '000905.XBHS'
# 持仓数量
g.stock_num = 20
# 记录上一次调仓的月份,用于控制月度调仓
g.last_month = 0
# 设定手续费(股票:万三,最低5元)
set_commission(commission_ratio=0.0003, min_commission=5.0, type="STOCK")
# 每天开盘后运行检查,判断是否需要调仓
run_daily(context, check_and_rebalance, time='09:35')
def check_and_rebalance(context):
"""
每日检查是否需要调仓(月度调仓逻辑)
"""
# 获取当前日期的月份
current_month = context.blotter.current_dt.month
# 如果当前月份与上一次调仓月份不同,则执行调仓
if current_month != g.last_month:
log.info("新的一月,开始执行 Fama-French 三因子选股调仓...")
rebalance(context)
g.last_month = current_month
def rebalance(context):
"""
核心调仓逻辑
"""
# 1. 获取股票池(中证500成分股)
# 注意:get_index_stocks 建议在盘前或盘中调用,不要在 initialize 中调用
universe = get_index_stocks(g.index_code)
if not universe:
log.warning("获取指数成分股失败")
return
# 2. 过滤 ST、停牌、退市股票
# filter_stock_by_status 返回的是剔除后的列表
target_list = filter_stock_by_status(universe, filter_type=["ST", "HALT", "DELISTING"])
if not target_list:
log.warning("过滤后股票列表为空")
return
# 3. 获取基本面数据:总市值 (total_value) 和 市净率 (pb)
# total_value 代表市值因子 (SMB),pb 代表价值因子 (HML)
# 注意:PTrade 的 get_fundamentals 返回的是 DataFrame
q_df = get_fundamentals(target_list, 'valuation', ['total_value', 'pb'])
if q_df is None or q_df.empty:
log.warning("获取基本面数据失败")
return
# 4. 数据清洗:去除空值和 PB <= 0 的数据(PB为负通常意味着资不抵债)
q_df = q_df.dropna()
q_df = q_df[q_df['pb'] > 0]
# 5. 计算因子排名 (Rank)
# 市值因子:越小越好 (SMB) -> 升序排名
q_df['cap_rank'] = q_df['total_value'].rank(ascending=True)
# 价值因子:PB 越小越好 (HML) -> 升序排名
q_df['pb_rank'] = q_df['pb'].rank(ascending=True)
# 6. 计算综合得分
# 简单的等权打分法:排名之和越小,说明市值越小且估值越低
q_df['total_score'] = q_df['cap_rank'] + q_df['pb_rank']
# 7. 选取排名前 N 的股票
q_df = q_df.sort_values(by='total_score', ascending=True)
buy_list = q_df.index[:g.stock_num].tolist()
log.info("本月选股列表: %s" % str(buy_list))
# 8. 执行交易
do_trade(context, buy_list)
def do_trade(context, buy_list):
"""
执行具体的买卖操作
"""
# 获取当前持仓
positions = context.portfolio.positions.keys()
# 卖出不在买入列表中的股票
for stock in positions:
if stock not in buy_list:
# 检查是否停牌,停牌无法卖出
stock_status = get_stock_status([stock], 'HALT')
if stock_status[stock] is False: # False 表示未停牌
order_target_value(stock, 0)
log.info("卖出: %s" % stock)
# 买入/调仓目标股票
if len(buy_list) > 0:
# 等权重分配资金
# 注意:这里简单使用总资产/股票数量,实际交易中可能需要预留少量现金防止手续费不够
position_value = context.portfolio.portfolio_value / len(buy_list)
for stock in buy_list:
# 检查是否停牌或涨停,避免废单(这里简单检查停牌,涨停由 order_target_value 内部机制或 check_limit 处理)
stock_status = get_stock_status([stock], 'HALT')
if stock_status[stock] is False:
order_target_value(stock, position_value)
# log.info("调整持仓: %s 到目标市值: %.2f" % (stock, position_value))
def handle_data(context, data):
"""
必须实现的函数,这里留空,逻辑主要在 run_daily 中
"""
pass
代码关键点解析
-
因子获取 (
get_fundamentals):- 我们使用了
valuation表。 total_value:A股总市值,用于衡量规模因子(SMB)。pb:市净率,用于衡量价值因子(HML)。Fama-French 原文使用的是账面市值比(B/M),它是 PB 的倒数。因此,PB 越低 等同于 B/M 越高,符合价值股特征。
- 我们使用了
-
数据处理与打分:
- 使用了
pandas的rank函数对两个因子分别进行排名。 - 直接将两个排名相加(
cap_rank + pb_rank),这是一种简单有效的多因子合成方法。得分越低,代表该股票在“小市值”和“低估值”两个维度上综合表现越好。
- 使用了
-
调仓控制:
- 利用
g.last_month变量记录上一次调仓的月份。 - 在
check_and_rebalance中比较current_month和g.last_month,实现自然月调仓(每月第一个交易日)。
- 利用
-
交易执行:
- 使用
order_target_value接口。这个接口非常适合定期调仓策略,它会自动计算买卖数量,将持仓调整到目标金额(即总资产的 1/N)。
- 使用
常见问题 (Q&A)
Q: 为什么选择中证500而不是沪深300?
A: Fama-French 模型中的 SMB 因子强调小市值股票的超额收益。沪深300代表大盘蓝筹,市值普遍较大,区分度不高。中证500包含了大量中盘和小盘股,更适合挖掘小市值因子的超额收益。
Q: 代码中如何处理停牌股票?
A: 代码中使用了 filter_stock_by_status 在选股阶段剔除停牌股。在交易阶段(do_trade),再次调用了 get_stock_status 检查持仓股是否停牌,防止对停牌股下单导致报错或废单。
Q: 可以增加动量因子吗?
A: 可以。如果增加动量因子(Momentum),这就变成了 Carhart 四因子模型。你需要获取过去一段时间(如12个月)的涨跌幅,将其加入打分逻辑中(通常是买入过去涨幅高的股票)。
Q: 回测时需要注意什么?
A: 建议将 get_history 或数据获取的 count 设置得足够大以确保数据稳定,但在本策略中主要依赖财务数据,因此要注意财务数据的滞后性。PTrade 的 get_fundamentals 在回测模式下会自动处理“未来函数”,即只能获取到回测日期当时已发布的财报数据,这是安全的。