问题描述
解决方案
这是一个基于PTrade平台的低市盈率(Low PE)与低市净率(Low PB)双低价值投资策略。
策略逻辑说明
- 选股范围:沪深300指数成分股(
000300.SS),确保流动性和基本面相对稳定。 - 选股指标:
- PE (TTM):滚动市盈率,衡量盈利能力与股价的关系。
- PB:市净率,衡量净资产与股价的关系。
- 筛选规则:
- 剔除停牌、ST、退市股票。
- 剔除PE为负(亏损)的股票。
- 双重排序:首先选取PE最低的前50只股票,然后在这些股票中选取PB最低的10只。这是一种经典的“双低”策略,旨在寻找盈利能力尚可且资产价格被低估的标的。
- 调仓频率:月度调仓。每个月第一个交易日进行选股和换仓。
- 资金分配:等权重买入。
策略代码
import pandas as pd
import numpy as np
def initialize(context):
"""
初始化函数,设置策略参数和全局变量
"""
# 设置股票池基准:沪深300
g.index_security = '000300.SS'
# 设置最大持仓数量
g.target_num = 10
# 记录上一个交易日的月份,用于判断是否换月
g.last_month = 0
# 换仓标志位
g.rebalance_flag = False
# 目标持仓列表
g.target_stocks = []
# 设置手续费:股票买卖万分之三,最低5元
set_commission(commission_ratio=0.0003, min_commission=5.0, type="STOCK")
# 设置滑点:0.2%
set_slippage(slippage=0.002)
def before_trading_start(context, data):
"""
盘前处理函数,每日开盘前运行
主要用于判断调仓日期和生成目标股票池
"""
# 获取当前回测/交易日期的月份
current_month = context.blotter.current_dt.month
# 判断是否换月(即每个月第一个交易日进行调仓)
if g.last_month != current_month:
g.rebalance_flag = True
g.last_month = current_month
else:
g.rebalance_flag = False
# 如果是调仓日,执行选股逻辑
if g.rebalance_flag:
# 1. 获取沪深300成分股
stocks = get_index_stocks(g.index_security)
# 2. 过滤掉ST、停牌、退市的股票
# filter_stock_by_status 默认过滤 ST, HALT, DELISTING
stocks = filter_stock_by_status(stocks)
if len(stocks) == 0:
log.warning("股票池为空,跳过选股")
g.target_stocks = []
return
# 3. 获取基本面数据:PE(TTM) 和 PB
# pe_ttm: 市盈率TTM, pb: 市净率
df = get_fundamentals(stocks, 'valuation', ['pe_ttm', 'pb'], date=context.blotter.current_dt)
if df is None or df.empty:
log.warning("未获取到财务数据")
g.target_stocks = []
return
# 4. 数据清洗与筛选
# 剔除PE为负(亏损)或为0的股票
df = df[df['pe_ttm'] > 0]
# 5. 双重排序逻辑
# 第一步:按PE从小到大排序,取前50名(初选低估值)
df = df.sort_values(by='pe_ttm', ascending=True)
df_temp = df.head(50)
# 第二步:在初选结果中,按PB从小到大排序,取前 g.target_num 名(优中选优)
df_final = df_temp.sort_values(by='pb', ascending=True).head(g.target_num)
# 更新目标持仓列表
g.target_stocks = df_final.index.tolist()
log.info("本月目标持仓股票: %s" % g.target_stocks)
def handle_data(context, data):
"""
盘中处理函数,主要用于执行交易
"""
# 仅在触发换仓标志时执行交易
if g.rebalance_flag:
# 1. 卖出不在目标池中的股票
# 获取当前所有持仓
current_positions = list(context.portfolio.positions.keys())
for stock in current_positions:
# 如果当前持仓股票不在新的目标列表中,且当前可卖(停牌股无法卖出,需注意)
if stock not in g.target_stocks:
# 检查是否停牌,非停牌才卖出
if data[stock].is_open:
order_target(stock, 0)
log.info("卖出非目标股票: %s" % stock)
# 2. 买入目标池中的股票
target_len = len(g.target_stocks)
if target_len > 0:
# 等权重分配资金
# 注意:这里简单使用总资产/目标数量,实际交易中可能需要预留少量现金防止手续费不足
value_per_stock = context.portfolio.portfolio_value / target_len
for stock in g.target_stocks:
# 检查是否停牌,非停牌才买入
if data[stock].is_open:
order_target_value(stock, value_per_stock)
# log.info("买入/调整股票: %s, 目标金额: %f" % (stock, value_per_stock))
# 交易完成后重置标志位,避免当日重复交易
g.rebalance_flag = False
代码关键点解析
-
get_index_stocks的位置:- PTrade建议在
before_trading_start中调用此函数获取成分股,而不是在initialize中,因为指数成分股会随时间变化。
- PTrade建议在
-
filter_stock_by_status:- 这是PTrade特有的便捷函数,用于一键过滤掉ST(风险警示)、HALT(停牌)、DELISTING(退市)的股票,防止买入无法交易或高风险的标的。
-
财务数据获取 (
get_fundamentals):- 使用了
valuation表中的pe_ttm(滚动市盈率) 和pb(市净率)。 pe_ttm > 0的过滤条件非常重要,因为负的市盈率代表公司亏损,不符合价值投资逻辑。
- 使用了
-
双重排序逻辑:
- 策略没有简单地将PE和PB相加,而是采用了漏斗筛选法:先选PE最低的一批,再从中选PB最低的。这能有效避免选出“PE极低但资产质量极差”或者“PB极低但盈利能力极差”的股票,平衡了盈利与净资产的估值。
-
Python 3.5 兼容性:
- 代码中使用了
%s格式化字符串,避免了 Python 3.6+ 才有的 f-string 语法,确保在旧版本 Python 环境中也能运行。
- 代码中使用了
常见问题 (Q&A)
Q: 为什么选择沪深300而不是全市场选股?
A: 全市场选股容易选中流动性差的小盘股或基本面有雷的股票。对于价值策略,沪深300成分股通常是行业龙头,财务数据相对规范,流动性好,更适合作为价值投资的基础池。
Q: 如何调整调仓频率?
A: 当前代码通过 if g.last_month != current_month: 实现月度调仓。如果想改为季度调仓,可以判断 current_month % 3 == 0。如果想改为每周调仓,可以使用 context.blotter.current_dt.weekday() 判断是否为周一。
Q: 为什么使用 order_target_value?
A: order_target_value 会自动计算需要买卖的股数,使该股票的持仓市值达到指定金额。这比计算股数后使用 order 更方便,能自动处理加仓和减仓操作。