问题描述
'603185.XSHG' 和 '601236.XSHG' 应该上市都不够五年,但都返回了数值,做排序什么的会有影响。怎么办?
df1 = get_factor_values(['603185.XSHG','601236.XSHG'], ['sales_growth'], end_date='2022-12-02', count=1)
display(pd.concat(df1))
q = query(indicator.eps, income.np_parent_company_owners, indicator.code).filter(
indicator.code.in_(['603185.XSHG','601236.XSHG'])
)
df3 = get_fundamentals(q, statDate='2018')
display('2018',df3)
df4 = get_fundamentals(q, statDate='2017')
display('2017',df4)
成长因子 growth
定义:0.18•long_term_predicted_earnings_growth + 0.11•short_term_predicted_earnings_growth + 0.24•earnings_growth + 0.47•sales_growth
解释
预期长期盈利增长率 long_term_predicted_earnings_growth:未来三年净利润分析师一致预期相对于净利润(不含少数股东损益)最新年报值的平均增长率。一个分析师预测的股票值为nan。
预期短期盈利增长率 short_term_predicted_earnings_growth:未来一年净利润分析师一致预期相对于净利润(不含少数股东损益)最新年报值的平均增长率,只有一个分析师预测的股票值为nan。
5年盈利增长率 earnings_growth:盈利增长率,过去5年的基本每股收益(basic_eps)关于[0,1,2,3,4]回归的斜率系数,然后再除以过去 5 年基本每股收益的均值的绝对值。
5年营业收入增长率 sales_growth:营收增长率,过去 5 年每股营业收入关于[0,1,2,3,4]回归的斜率系数,然后再除以过去 5 年每股营业收入的均值的绝对值。对于保险行业的股票,使用“已赚保费”代替“销售收入”计算每股营业收入,对于银行业的股票,sales_growth为nan。
earnings_growth和sales_growth至少需要有4年的财务数据,否则为nan。
解决方案
您好!您遇到的这个问题在量化数据处理中非常典型。简单来说,“上市时间不足5年”并不等于“财务数据不足4年”。
以下是详细的原因分析以及在策略中规避此问题的解决方案。
一、 为什么上市不足5年的股票会有数值?
根据 JoinQuant 官方文档对 growth 风格因子的定义:
sales_growth至少需要有 4年 的财务数据,否则为 nan。
这里强调的是财务数据的年限,而不是上市时间的年限。
在中国 A 股市场,公司在进行 IPO(首次公开募股)时,招股说明书中通常强制要求披露过去 3 年的完整财务数据。这意味着,当一只股票敲钟上市的第一天,数据库中就已经包含了它过去 3 年的年度财务数据。
- 举个例子:假设某公司 2021 年上市,那么它在 2021 年就已经有了 2018、2019、2020 年的财务数据。
- 当它上市满 1 年(到了 2022 年发布年报后),它在数据库中就拥有了 2018、2019、2020、2021 年共计 4年 的财务数据。
此时,它已经满足了 JoinQuant 因子库中 sales_growth 因子“至少需要有4年的财务数据”的计算条件,因此底层算法会利用这 4 年的数据进行线性回归计算斜率,从而返回一个具体的数值,而不是 NaN。
二、 这种现象对排序的影响及解决办法
由于包含了上市前的数据,且次新股在上市前后的业绩波动特征往往与成熟期企业不同,直接将它们与老股放在一起进行因子排序(Rank)确实可能会产生偏差。
最佳的解决办法是:在获取因子值或进行排序之前,主动在股票池中剔除上市时间不足 N 年的股票(即剔除次新股)。
在量化策略中,剔除次新股、ST股、停牌股是标准的数据清洗流程。您不能完全依赖因子本身返回 NaN 来过滤股票。
Python 代码解决方案:主动过滤次新股
您可以使用 JoinQuant 提供的 get_security_info API 获取股票的上市日期 (start_date),然后计算其上市天数,将上市不足 5 年(或您认为合适的年限)的股票从列表中剔除。
import pandas as pd
import datetime
from jqdata import *
def filter_new_stocks(security_list, current_date, min_listed_days=365*5):
"""
过滤掉上市时间不足 min_listed_days 天的股票
:param security_list: 原始股票代码列表
:param current_date: 当前逻辑日期 (datetime.date 或 datetime.datetime)
:param min_listed_days: 最小上市天数,默认 5 年 (约 1825 天)
:return: 过滤后的股票代码列表
"""
valid_stocks = []
for stock in security_list:
info = get_security_info(stock)
if not info:
continue
# 计算上市至今的天数
listed_days = (current_date - info.start_date).days
# 如果上市天数大于等于设定的阈值,则保留
if listed_days >= min_listed_days:
valid_stocks.append(stock)
return valid_stocks
# ================= 测试示例 =================
# 假设当前回测/研究日期为 2022-12-02
current_dt = datetime.date(2022, 12, 2)
# 原始股票池,包含您提到的两只次新股和一只老股(平安银行)
raw_stocks = ['603185.XSHG', '601236.XSHG', '000001.XSHE']
# 1. 过滤掉上市不足 5 年 (1825天) 的股票
filtered_stocks = filter_new_stocks(raw_stocks, current_dt, min_listed_days=1825)
print(f"当前日期: {current_dt}")
print(f"原始股票池: {raw_stocks}")
print(f"过滤上市不足5年的股票后: {filtered_stocks}")
# 2. 仅对过滤后的老股获取 sales_growth 因子
if filtered_stocks:
df_factor = get_factor_values(filtered_stocks, ['sales_growth'], end_date=current_dt, count=1)
print("\n因子值获取结果:")
display(df_factor['sales_growth'])
else:
print("\n没有符合条件的股票。")
总结
- 机制正常:JoinQuant 因子库返回数值是符合其逻辑的,因为包含了招股书披露的上市前财务数据,满足了“至少4年数据”的条件。
- 主动清洗:为了避免次新股的财务特征干扰您的横截面排序(Cross-sectional Ranking),强烈建议在策略的
before_trading_start或选股逻辑中,封装一个filter_new_stocks函数,主动将上市不足 3 年或 5 年的股票剔除出您的候选股票池(Universe)。