问题描述
解决方案
在PTrade中,筛选并排除商誉占比过高的股票主要依赖于 get_fundamentals 函数来获取财务数据。
通常我们使用 商誉占净资产的比例(商誉 / 所有者权益合计)来衡量风险。如果该比例过高(例如超过10%或20%),则意味着公司资产中“虚”的部分较多,存在商誉减值暴雷的风险。
实现思路
- 获取股票池:确定要筛选的范围(如沪深300、全A股等)。
- 获取财务数据:使用
get_fundamentals查询balance_statement表中的good_will(商誉)和total_shareholder_equity(所有者权益合计/净资产)。 - 数据清洗:将商誉为
NaN(空值)的填充为0,因为没有商誉的公司是安全的。 - 计算比例:计算
商誉 / 净资产。 - 过滤:保留比例小于设定阈值(如 0.1)的股票,剔除高商誉股票。
- 更新股票池:使用
set_universe更新当天的交易标的。
策略代码实现
以下是一个完整的策略示例,展示了如何在每日盘前(before_trading_start)进行商誉筛选。
def initialize(context):
"""
初始化函数
"""
# 设定基准指数,例如沪深300
g.index = '000300.SS'
# 设定商誉占比阈值,这里设定为 10% (0.1)
# 如果商誉/净资产 > 10%,则排除该股票
g.goodwill_threshold = 0.10
# 初始设置股票池
set_universe(get_index_stocks(g.index))
def before_trading_start(context, data):
"""
盘前处理函数:每日开盘前筛选股票
"""
# 1. 获取待筛选的股票列表
current_stocks = get_index_stocks(g.index)
# 2. 获取财务数据
# 查询字段:good_will(商誉), total_shareholder_equity(所有者权益合计/净资产)
# date参数传入当前回测日期,获取最近发布的财报数据
q_date = context.blotter.current_dt.strftime("%Y%m%d")
df = get_fundamentals(
current_stocks,
'balance_statement',
['good_will', 'total_shareholder_equity'],
date=q_date
)
if df is not None and not df.empty:
# 3. 数据清洗
# 很多公司没有商誉,数据库中可能为NaN,将其填充为0
df['good_will'] = df['good_will'].fillna(0)
# 过滤掉净资产为0或负数的公司(资不抵债),避免除零错误或逻辑错误
df = df[df['total_shareholder_equity'] > 0]
# 4. 计算商誉占比 (商誉 / 净资产)
df['gw_ratio'] = df['good_will'] / df['total_shareholder_equity']
# 5. 筛选符合条件的股票
# 保留商誉占比小于阈值的股票
valid_df = df[df['gw_ratio'] < g.goodwill_threshold]
valid_stocks = valid_df.index.tolist()
# 6. 更新股票池
if len(valid_stocks) > 0:
set_universe(valid_stocks)
log.info("原始股票数量: %s, 排除高商誉后数量: %s" % (len(current_stocks), len(valid_stocks)))
else:
log.warning("筛选后无符合条件的股票,保持原股票池或清空")
else:
log.warning("未获取到财务数据,跳过筛选")
def handle_data(context, data):
"""
盘中运行函数
"""
# 这里可以编写具体的买卖逻辑
# 示例:简单的买入操作,仅作演示
# 此时 context.universe 已经是剔除了高商誉股票后的列表
# 获取当前股票池
universe = context.get_universe()
# 简单的示例逻辑:买入股票池中的第一只股票
if len(universe) > 0:
target = universe[0]
# 简单的仓位控制
if len(context.portfolio.positions) == 0:
order_target_value(target, context.portfolio.cash)
代码关键点解析
get_fundamentals: 这是核心API。table='balance_statement': 商誉数据位于资产负债表。fields=['good_will', 'total_shareholder_equity']: 我们需要这两个字段来计算比例。
fillna(0): 这一点非常重要。大多数公司(特别是银行、传统制造业)可能没有商誉,数据库返回NaN。如果不填充为0,这些优质公司可能会在计算或比较时被错误排除。- 阈值设定:
g.goodwill_threshold = 0.10是一个经验值。你可以根据风险偏好调整为 0.05 (严格) 或 0.20 (宽松)。 - 执行时机: 放在
before_trading_start中执行,确保当天的handle_data交易逻辑使用的是已经清洗过的安全股票池。
Q&A
Q1: 为什么要用“净资产”而不是“总资产”作为分母?
A1: 使用净资产(所有者权益)作为分母更为保守和严格。商誉是收购溢价产生的,如果商誉发生减值,直接扣减的是利润,最终侵蚀的是净资产。商誉占净资产比例过高,意味着一旦发生减值,对股东权益的打击是毁灭性的。当然,你也可以使用 total_assets(总资产)作为分母,只需将代码中的 total_shareholder_equity 替换为 total_assets 即可。
Q2: get_fundamentals 获取的是什么时候的数据?
A2: 在回测模式下,如果不指定 date,默认获取回测当前日期之前最近一次发布的财报数据,避免了未来函数。在代码中显式传入 date=context.blotter.current_dt 是为了确保逻辑的清晰性,PTrade底层会自动处理财报发布的滞后性(即只会获取截止到当天已公告的数据)。
Q3: 如果我想查看具体的商誉数值,怎么打印?
A3: 可以在 before_trading_start 中添加日志输出。例如:
# 打印商誉占比最高的前5名
top_risk = df.sort_values(by='gw_ratio', ascending=False).head(5)
log.info("高商誉风险股票前5名:\n%s" % top_risk[['good_will', 'gw_ratio']])