问题描述
解决方案
这是一个非常经典的价值投资选股策略。在PTrade中实现筛选“连续三年ROE大于15%”的股票,主要涉及以下几个关键点:
- 获取财务数据:使用
get_fundamentals函数。 - 指定数据表和字段:使用
profit_ability(盈利能力表)中的roe(净资产收益率)字段。 - 指定报表类型:必须使用
report_types='4',代表筛选年报数据。 - 动态计算年份:为了避免“未来函数”(即在财报公布前就用到了财报数据),需要根据当前回测日期动态判断应该读取哪几年的年报。通常年报在次年的4月30日前公布。
以下是完整的策略代码实现:
策略代码
def initialize(context):
"""
初始化函数
"""
# 设置股票池为空,稍后在 before_trading_start 中筛选
g.security = []
set_universe(g.security)
# 设置手续费(可选)
set_commission(commission_ratio=0.0003, min_commission=5.0, type="STOCK")
# 设定一个全局变量,用于记录上一次筛选的年份,避免每天重复查询大量数据
g.last_check_year = 0
def before_trading_start(context, data):
"""
盘前处理:每天开盘前运行,用于筛选股票
"""
current_dt = context.blotter.current_dt
current_year = current_dt.year
current_month = current_dt.month
# ----------------------------------------------------------------
# 1. 确定要查询的年份范围
# ----------------------------------------------------------------
# 逻辑:年报通常在次年4月30日前披露完毕。
# 如果当前月份 >= 5月,说明去年的年报已经出齐,可以查 [去年, 前年, 大前年]
# 如果当前月份 < 5月,去年的年报可能还没出,只能查 [前年, 大前年, 大大前年]
if current_month >= 5:
end_report_year = current_year - 1
else:
end_report_year = current_year - 2
start_report_year = end_report_year - 2 # 往前推2年,共3年
# ----------------------------------------------------------------
# 2. 避免每天重复计算,仅在年份变化或关键月份时重新筛选
# 这里为了演示逻辑,简化为:如果计算出的截止年份变了,就重新筛选
# ----------------------------------------------------------------
if end_report_year == g.last_check_year:
return
log.info("开始筛选 %s 至 %s 年连续三年ROE > 15%% 的股票" % (start_report_year, end_report_year))
# 获取全市场A股代码
all_stocks = get_Ashares()
# ----------------------------------------------------------------
# 3. 查询财务数据
# ----------------------------------------------------------------
# 查询盈利能力表(profit_ability)中的ROE字段
# report_types='4' 代表年报
# 注意:一次性查询全市场几千只股票的3年数据可能会比较慢或超时,
# 实际实盘中建议分批查询或缩小范围。这里为演示直接查询。
fundamentals = get_fundamentals(
security=all_stocks,
table='profit_ability',
fields='roe',
start_year=str(start_report_year),
end_year=str(end_report_year),
report_types='4'
)
selected_stocks = []
# ----------------------------------------------------------------
# 4. 处理数据并筛选
# ----------------------------------------------------------------
if fundamentals is not None:
# PTrade Python3.5环境下,按年份范围查询返回的是 Panel 对象
# Items轴是股票代码,Major轴是时间(年份),Minor轴是字段(roe)
# 如果是 Python3.11 环境,返回的可能是 MultiIndex DataFrame,需根据实际环境调整
# 这里按标准 Panel 结构处理
if hasattr(fundamentals, 'items'):
for stock in fundamentals.items:
# 获取该股票的 DataFrame (Index=日期, Column=roe)
df = fundamentals[stock]
# 过滤掉数据不全的股票(必须有3年的数据)
if len(df) < 3:
continue
# 核心筛选逻辑:判断这3年的ROE是否都大于15
# PTrade中 roe 字段通常是百分比数值(如 15.5 代表 15.5%)
# 使用 min() 函数判断最小的一年是否也大于 15
roe_values = df['roe'].values
if min(roe_values) > 15:
selected_stocks.append(stock)
log.info("筛选完成,共选出 %d 只股票" % len(selected_stocks))
# 更新全局股票池和 PTrade 股票池
g.security = selected_stocks
set_universe(g.security)
# 更新检查年份标记
g.last_check_year = end_report_year
def handle_data(context, data):
"""
盘中处理:简单的买入持有逻辑
"""
if not g.security:
return
# 简单的等权重买入逻辑
# 1. 卖出不在股票池中的股票
for stock in context.portfolio.positions:
if stock not in g.security:
order_target(stock, 0)
# 2. 买入股票池中的股票(等权重)
if len(g.security) > 0:
# 计算每只股票应该分配的资金
target_value = context.portfolio.portfolio_value / len(g.security)
for stock in g.security:
# 过滤停牌、跌停等无法买入的情况
if data[stock].is_open == 0:
continue
# 调仓到目标市值
order_target_value(stock, target_value)
代码逻辑详解
-
时间窗口的选择 (
before_trading_start):- 策略的核心难点在于避免未来函数。
- 代码中判断
current_month >= 5。因为根据A股规定,上市公司必须在4月30日前披露上一年度的年报。 - 如果在5月1日之后,我们可以安全地使用去年的年报数据。
- 如果在5月1日之前,去年的数据可能不全,为了保证连续性,我们退而求其次,使用前年、大前年、大大前年的数据。
-
数据获取 (
get_fundamentals):table='profit_ability':ROE数据位于盈利能力表中。report_types='4':非常重要,这指定了我们只看年报。如果不加这个参数,可能会混入季报数据,导致逻辑错误(季报的ROE通常不能直接和年报ROE比较,除非做年化处理)。start_year和end_year:用于指定获取数据的年份区间。
-
数据筛选:
get_fundamentals在查询多只股票、多个年份时,返回的数据结构通常是pandas.Panel(在PTrade Python 3.5环境中)。- 我们遍历每只股票,检查其返回的数据长度是否为3(确保三年都有数据)。
- 使用
min(roe_values) > 15来确保每一年的ROE都超过15%。
-
交易执行 (
handle_data):- 这是一个基本的等权重调仓逻辑。如果股票被剔除出名单,就卖出;如果在名单内,就调整持仓至平均市值。
常见问题提示
- 数据量问题:
get_fundamentals一次性查询全市场4000+只股票的3年数据可能会比较耗时或触发接口限制。如果遇到超时问题,可以将all_stocks切片分批查询。 - ROE单位:PTrade中的
roe字段单位通常是百分比数值(例如返回18.5代表18.5%),因此代码中直接与15比较是正确的。 - Python版本:上述代码基于PTrade标准的Python 3.5环境编写(使用Panel结构)。如果您的券商环境已升级至Python 3.11,
get_fundamentals返回的将是DataFrame(多级索引),此时遍历逻辑需要微调(通常使用df.groupby(level=0)进行遍历)。
Q&A
Q: 为什么不使用 get_history 来获取ROE?
A: get_history 主要用于获取行情数据(如开盘价、收盘价、成交量)。ROE属于财务数据,必须通过 get_fundamentals 获取。
Q: 如何筛选“扣非ROE”?
A: 在 get_fundamentals 中,可以将字段 roe 改为 roe_cut(净资产收益率%扣除摊薄),这通常更能反映企业主营业务的盈利能力。
Q: 策略回测时为什么5月之前买入的是前年的优绩股?
A: 这是为了严谨性。在现实世界中,直到4月底年报披露完毕前,我们无法确切知道所有公司去年的完整ROE。为了防止回测利用“未公开信息”作弊,必须等待年报披露期结束后再使用当年的数据。