问题描述
解决方案
这是一个非常经典的多因子选股策略需求。在量化交易中,结合市盈率(PE)、**市净率(PB)和净资产收益率(ROE)**通常被称为“低估值高质量”策略。
策略设计思路
- 选股范围(股票池):通常选择沪深300或中证500作为基础股票池,这里我们以沪深300为例。
- 因子选择:
- PE (TTM):滚动市盈率,越低越好(代表估值低)。
- PB:市净率,越低越好(代表估值低)。
- ROE (TTM):滚动净资产收益率,越高越好(代表盈利能力强)。
- 数据处理:
- 剔除停牌、ST、退市股票。
- 剔除因子数据缺失的股票。
- 打分法:对每个因子进行排名。PE和PB按升序排名(值越小排名越靠前),ROE按降序排名(值越大排名越靠前)。将三个排名相加得到总分,总分越低越好。
- 调仓周期:按月调仓(例如每个月第一个交易日)。
- 资金分配:等权重买入排名前N只股票。
PTrade 策略代码实现
以下是完整的策略代码。代码基于Python 3.5语法编写,可以直接在PTrade的回测或交易环境中运行。
import pandas as pd
import numpy as np
def initialize(context):
"""
初始化函数,设置策略参数
"""
# 1. 设置基准指数:沪深300
g.index_security = '000300.SS'
# 2. 设置持仓数量
g.stock_num = 10
# 3. 设置调仓标志,默认为False
g.rebalance_flag = False
# 设置回测参数(仅回测有效,实盘会忽略)
set_commission(commission_ratio=0.0003, min_commission=5.0, type="STOCK")
set_slippage(slippage=0.002)
def before_trading_start(context, data):
"""
盘前处理函数,每天开盘前运行
"""
# 获取当前日期
current_date = context.blotter.current_dt.date()
# 判断是否为月初(每月第一个交易日调仓)
# 获取未来几天的交易日,如果当前日期是本月第一个交易日,则标记为调仓日
# 这里使用一种简化的判断逻辑:获取过去20个交易日,判断月份是否发生变化
previous_date = get_trading_day(-1)
if previous_date.month != current_date.month:
g.rebalance_flag = True
else:
g.rebalance_flag = False
def handle_data(context, data):
"""
盘中运行函数
"""
# 如果是调仓日,则执行选股和交易逻辑
if g.rebalance_flag:
rebalance(context)
# 执行完后重置标志
g.rebalance_flag = False
def rebalance(context):
"""
核心调仓逻辑
"""
log.info("开始进行多因子选股调仓...")
# 1. 获取股票池(沪深300成分股)
# 注意:get_index_stocks在回测中默认取回测当日成分股
stocks = get_index_stocks(g.index_security)
# 2. 过滤掉 ST、停牌、退市的股票
# filter_stock_by_status 返回的是剔除后的列表
stocks = filter_stock_by_status(stocks, filter_type=["ST", "HALT", "DELISTING"])
if not stocks:
log.warning("股票池为空,跳过本次调仓")
return
# 3. 获取财务数据
# PTrade的get_fundamentals一次只能查一张表,所以需要分两次查询再合并
# 3.1 查询估值数据:PE(TTM) 和 PB
# pe_ttm: 市盈率TTM, pb: 市净率
df_val = get_fundamentals(
stocks,
table='valuation',
fields=['pe_ttm', 'pb']
)
# 3.2 查询盈利能力数据:ROE(TTM)
# roe_ttm: 净资产收益率TTM
df_prof = get_fundamentals(
stocks,
table='profit_ability',
fields=['roe_ttm']
)
# 4. 数据合并与清洗
if df_val is None or df_prof is None:
log.warning("获取财务数据失败")
return
# 将两个DataFrame合并,索引通常是股票代码
# 注意:PTrade返回的DataFrame索引通常是股票代码
df = pd.concat([df_val, df_prof], axis=1, join='inner')
# 去除包含空值的行
df = df.dropna()
# 过滤掉PE为负(亏损)的股票,通常亏损股的估值指标会失真
df = df[df['pe_ttm'] > 0]
if len(df) == 0:
log.warning("数据清洗后无股票可选")
return
# 5. 多因子打分 (Rank Scoring)
# 策略:低PE、低PB、高ROE
# 对PE进行排名,从小到大,名次越小越好
df['rank_pe'] = df['pe_ttm'].rank(ascending=True)
# 对PB进行排名,从小到大,名次越小越好
df['rank_pb'] = df['pb'].rank(ascending=True)
# 对ROE进行排名,从大到小,名次越小越好 (ascending=False)
df['rank_roe'] = df['roe_ttm'].rank(ascending=False)
# 计算总分(等权重相加)
df['total_score'] = df['rank_pe'] + df['rank_pb'] + df['rank_roe']
# 6. 选出总分最低(排名最靠前)的N只股票
df_sorted = df.sort_values(by='total_score', ascending=True)
target_stocks = df_sorted.head(g.stock_num).index.tolist()
log.info("选入股票: %s" % target_stocks)
# 7. 执行交易
adjust_position(context, target_stocks)
def adjust_position(context, target_stocks):
"""
交易执行函数
"""
# 获取当前持仓
current_positions = list(context.portfolio.positions.keys())
# 1. 卖出不在目标列表中的股票
for stock in current_positions:
if stock not in target_stocks:
# 检查是否停牌,停牌无法卖出
if check_limit(stock).get(stock) == 0: # 0表示既不涨停也不跌停,这里简单判断,严谨可用get_stock_status
# 强行平仓
order_target_value(stock, 0)
# 2. 买入目标股票
# 计算每只股票的目标资金分配(等权重)
if len(target_stocks) > 0:
# 获取当前账户总资产
total_value = context.portfolio.portfolio_value
# 预留一点现金防止手续费不够,乘以0.98
target_value_per_stock = (total_value * 0.98) / len(target_stocks)
for stock in target_stocks:
# 下单调整到目标金额
order_target_value(stock, target_value_per_stock)
代码关键点解析
-
数据获取 (
get_fundamentals):- PTrade的财务数据分布在不同的表中。市盈率 (
pe_ttm) 和市净率 (pb) 在valuation表中,而净资产收益率 (roe_ttm) 在profit_ability表中。 - 代码中分别调用了两次接口,然后使用
pd.concat将数据合并,这是处理多表因子的标准做法。
- PTrade的财务数据分布在不同的表中。市盈率 (
-
因子排序 (
rank):- 我们没有直接使用因子的绝对值进行加权,而是使用了排名法。
rank(ascending=True):值越小,排名越靠前(得分越低)。适用于PE和PB。rank(ascending=False):值越大,排名越靠前(得分越低)。适用于ROE。- 最后将三个排名相加,总分越低代表综合表现越好。
-
调仓逻辑:
- 在
before_trading_start中通过比较昨日和今日的月份来判断是否是月初。 - 使用
order_target_value进行下单,这会自动计算需要买入或卖出的股数,非常适合等权重策略。
- 在
-
风险控制:
- 使用了
filter_stock_by_status剔除了ST、停牌和退市股票,防止买入无法交易或风险过大的标的。 - 剔除了PE为负值的亏损股票。
- 使用了
常见问题 (Q&A)
Q: 为什么使用 TTM (Trailing Twelve Months) 数据?
A: TTM 数据包含了最近12个月的财务数据,相比静态数据(仅看上一年报)更能反映公司当前的经营状况,且消除了季节性波动的影响。
Q: 如何修改选股数量?
A: 修改 initialize 函数中的 g.stock_num = 10,将 10 改为你想要的数量即可。
Q: 我想加入市值因子(小市值策略),怎么改?
A: 在 valuation 表中获取 total_value (总市值) 字段,对其进行 rank(ascending=True) 排名,并加入到 total_score 的计算中即可。
Q: 为什么代码中没有 import ptrade?
A: PTrade 平台的策略代码运行在特定的沙箱环境中,所有 API(如 get_fundamentals, order 等)都是内置的全局函数,无需导入,直接调用即可。
策略与金融概念
什么是多因子选股?
多因子选股是一种量化投资策略,它假设股票的收益可以由一组共同的“因子”来解释。通过寻找那些在历史上能够产生超额收益的因子(如低估值、高盈利、高动量等),并结合这些因子对股票进行打分或排序,从而构建投资组合。
为什么同时看PE、PB和ROE?
这三个指标构成了一个经典的“高性价比”筛选体系:
- PE (市盈率):衡量买入成本与公司盈利的关系。低PE意味着你用较少的钱买到了较多的盈利。
- PB (市净率):衡量买入成本与公司净资产的关系。低PB意味着股价接近甚至低于公司的账面价值,具有安全边际。
- ROE (净资产收益率):衡量公司利用股东资本赚钱的能力。高ROE意味着公司也是“印钞机”。
结合逻辑:如果只看低PE/PB,可能会买到基本面恶化的“垃圾股”;加上高ROE,就确保了买入的是既便宜又赚钱的好公司。