跳转至



factor&strategy »

不看懂这篇文章,不要在量化中使用市盈率!


在9月16日的公众号文章(《节前迎来揪心一幕!谁来告诉我,A股现在有没有低估?》),我们发表了这样一个数据,就是A股的市盈率已经处于历史低位(10%分位)。两天之后,从9月18日起,沪指迎来一轮长达两个月的小牛市。

当沪指涨回到3452时,现在的市盈率又处在哪个位置?这是我们今天要回答的第一个问题。

不过,今天的重点是研究市盈率作为量化因子的有效性。我们将提出一个暂新的视角,分别在个股及沪指上,得出有趣的结论。

市盈率因子

市盈率在量化中一般被称为估值因子。它是价值因子的一个子类。价值因子包含了账面市值比,盈利因子(ROE),投资因子等。

市盈率的计算公式如下:

\[\text{PE} = \frac{\text{股票价格}}{\text{每股收益}}\]

一般认为,低估值的公司有可能股价上涨,高估值的公司有可能股价下跌,从而完成价格对价值的回归。

但实际上,PE作为因子,坑很多。

  1. PE值依赖每股收益这一财务指标,它的发布周期是季度,因此在两次数据发布之间,它只携带了收盘价这样的噪声信息(相对因子而言)。
  2. 一般而言,因子分析是横截面分析,即是一种同一时间点上、不同资产的相同属性放在一起比较排序的分析方法。但是,资产的PE差别主要由行业决定,而不是资产自身决定。所以,把PE当成因子来求资产的 alpha, 必须进行行业中性化。
  3. 周期性行业,往往是高PE值(甚至为负数时)是买入时机,因为往后公司盈利会好转,筹码后面才有派发的机会;低PE值时则是卖出时机,因为往后公司盈利会下降,坏消息不断,这时筹码派发就只能降价派发。

不过,眼见为实。我们还是拿它来回测一下。在回测时,我们要使用PE_TTM指标而不是PE,PE_TTM是滚动市盈利。PE是以年为单位发布的,这使得它的数据响应不够及时。PE_TTM在时间周期上,它虽然仍是以一年为单位进行计算,但是它是在 4 个季度的滑动窗口上计算和发布的,所以,更能及时反映公司的盈利变化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
start = datetime.date(2018, 1, 1)
end = datetime.date(2023, 12, 31)
universe = get_stock_list(start, code_only=True)
barss = load_bars(start, end, tuple(universe))

pe = get_daily_basic(["pe_ttm"], start, end, universe) * -1
prices = barss.price.unstack(level="asset")

merged = get_clean_factor_and_forward_returns(pe, prices)
create_returns_tear_sheet(merged)

这里的get_daily_basic是我们封装的tushare函数,对应着tushare的daily_basic函数,但它可以一次把某个指标的数据取全。

注意第6行,我们给PE乘上了-1。如果你不明白这是为什么,也许可以考虑学习《因子分析与机器学习策略》这门课。

这是回测的年化Alpha。在我们的公众号上,曾出现过好多次年化Alpha超过15%的因子 -- 它们都是从无数次失败中选出来的 -- 实际上很多因子都跟PE一样:

PE因子的年化Alpha

分层均值收益图也表明该因子不具有与收益的良好线性关系。

碰壁之后,我们再回过头来思考为什么。

首先,PE是充满噪声的数据。它实际上在一年的时间里,在一个长达250个数据点的样本中,只有4次携带了信息。当我们使用PE作为因子时,实际上是在使用CLOSE作为因子。

降噪之后,发现周期股交易信号

所以,我们下面来给PE去噪声。去噪声的方法就是将PE除以收盘价。这样得到的实际上是每股收益的倒数。

生猪行业是一个强周期的行业。我们选其中一个看看:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def deepinsight_fundamental(ticker: str, field, start, end, inverse = False):
    df = get_daily_basic([field, "close"], start, end, (ticker,))
    df = df.xs(ticker, level="asset")

    if inverse:
        df[field] = df["close"] / df[field]
    else:
        df[field] = df[field] / df["close"]

    df["d1"] = df[field].diff()
    df.ffill(inplace=True)
    _, ax = plt.subplots(figsize=(15, 5))
    ax.plot(df.index, df["d1"], label=f"{field}_denoise")
    ax2 = ax.twinx()
    ax2.plot(df.index, df["close"], label="close", color="r")
    ax.legend(loc=2)
    ax2.legend(loc=1)
    plt.show()

start = datetime.date(2005, 1, 1)
end = datetime.date(2014, 10,31)
deepinsight_fundamental("002714.XSHE", "pe_ttm", start, end)
某生🐖企业周期图 2005-2014

这是2005年到2014年间的结果。我们看到,大约在2014年4月前后,出现一个明显的信号,随后该资产价格上涨了60%(由于tushare数据在此处未复权,实际上涨更多)。

我们再来看2015到2022年的结果:

1
2
3
start = datetime.date(2014, 1, 1)
end = datetime.date(2022, 12, 31)
deepinsight_fundamental("002714.XSHE", "pe_ttm", start, end)
某生🐖企业周期图 2014-2022

在一连串的大于零的信号中,我们关注第一个,它是买入的起涨点;在一连串的于小零的信号中,我们也关注第一个,它是下跌起点(因为季度发布时间关系,有可能滞后一点)

在上图中,我们看到,2018年9月左右,出现比较明显的买入时机,随后上涨约4倍,直到2019三季度出现强烈的卖出信号(小于-3的那根线)。此后也随即出现下跌,下跌一直持续到2020年初。此后虽然股份有过上涨,也有过波动,但信号上以弱卖出居多。

Tip

这个方法是为强周期股准备的。不适用于弱周期股。

我们已经揭示了非常有趣的一个谜底。现在,我们来回答第一个问题,沪指上涨到3450之后,现在的市盈率是高还是低?

3450点,现在沪指高了吗?

我们总是通过数据来说话。先取沪指数据:

1
2
3
4
5
6
7
import akshare as ak

pe = ak.stock_market_pe_lg(symbol="上证")
pe.set_index("日期", inplace=True)
pe.index.name = "date"
pe.rename(columns={"平均市盈率": "pe", "指数": "price"}, inplace=True)
pe.tail(15)

表1 市盈率与指数

这样我们就得到了1999年以来沪指全部市盈率数据。我们用同样的方法来查看一下全市场的盈利能力。

1
2
3
4
a = pe.copy()
a["adj"] = a.price/a.pe
a = a[["price", "adj"]]
a.plot(secondary_y='price', figsize=(15,5))
去掉噪声后的PE与指数走势

从图中可以看出,这么多年以来,A股的盈利能力(以下称指标)一直是在上涨的。但是速度略有变化。在2004年到2012年间,上涨速度较快,最终带动了指数的回归,由跌转涨,不过在6000点的高位还是有点过度反应。2016年之后,指标走势变平缓,股价也受此因素影响,这也是万年三千点的真实来由。

从2016年初到2024年初,这一指标上涨了25%,当时是2737点,上涨25%就是3421点左右。这是大的趋势来看,它不会很精确,只是一种猜想。

最后,我们来回答3450点的沪指,现在处在2016年以来的哪个分位数上。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
start = datetime.date(2016, 1, 1)
pe2016 = pe.loc[start:]

rank = pe2016.rank().loc[:, "pe"]
percentile = rank / len(pe2016)
percentile.plot()

p30 = 0.3
p70 = 0.7

# 30和70分位
plt.axhline(y=p30, color='green', linestyle='--', label='30th Percentile')
plt.axhline(y=p70, color='red', linestyle='--', label='70th Percentile')

# 获取最后一期的PE值及其日期
last_pe = pe2016['pe'].iloc[-1]
last_date = pe2016.index[-1]

# 在图上标注最后一期的PE值
plt.annotate(f'PE/分位: {last_pe:.2f}/{percentile.iloc[-1]:.1%}', 
             xy=(last_date, percentile.iloc[-1]), 
             xytext=(last_date, percentile.iloc[-1] + 0.05), 
             arrowprops=dict(facecolor='red', shrink=0.05))

# 添加图例
plt.legend()

# 显示图形
plt.show()

数据表明,当前处在2016年以来的54%分位,现在处在进可攻、退可守的区间,没有明显的指标压制,也没有提供进攻的势能。在这个点上怎么操作,交给别的指标吧!