选股:周期性股票的交易方式-席勒市盈率

Note

本来是放在quail.ink微信公众号的收费文章,想了想,还是算了


周期调整市盈率(CAPE)

周期调整市盈率(CAPE),又叫席勒市盈率(Shiller’s PE) ,是耶鲁大学、2013年诺贝尔经济学奖得主,教授罗伯特‧席勒,发明的。这指标厉害的地方在于它成功地预警了2000年美国互联网泡沫危机。

原理

席勒市盈率,实际上是通过平均过去10年的净利润来代替普通市盈率中的单一净利润,以此来平滑经济周期对估值的影响。说到底,其原理就像我们经常用到的技术性指标,简单移动平均线(SMA)。其目的都是平滑某段时间的波动性(周期性)。

唐朝在分众传媒上的实践

唐朝(老唐),作为资深的投资者,在其出版的《价值投资实战手册》中以总结的形式介绍了席勒市盈率在股票分众传媒(002027) 上的使用原因和实操方法。

老唐最开始在股票分众传媒(002027) 上的误判,导致了老唐几乎在股价最高位上买入。后面通过不断地反思后意识到,分众传媒(002027) 是属于周期性股票,其净利润的预测应该是要符合宏观经济周期的运行原则。

因此,老唐的操作方式如下:

  1. 统计分众传媒(002027) 2010-2019年10年的净利润累计约303亿元,平均年度“正常产出”约30亿元;
  2. 接下来,按照老唐估值法在无风险收益率处于3%4%时,合理市盈率取值2530倍,即 PE = 1/无风险收益率;
  3. 因此当年合理估值在750亿~900亿元(30亿 x PE),或写作“825±10%”范围;
  4. 确定买入卖出的合理估值范围。买入:按照当年合理估值的七折买入,合理估值上限的150%卖出,买人位置为825x70%=578 亿元,卖出位置为 825 x110% x150%=1360 亿元。

本质

估值方法形形色色、千变万化,并没有对与错之分。

但席勒市盈率估值法本质是均值回归,属于择时指标,而不是选股指标,运用此指标的大前提是好企业。先有好企业,再考虑好价格。

所以在使用的时候需要慎重。

Python量化

老唐在使用席勒市盈率估值法的时候给出了非常详细的操作方式,那么我们可以通过Python编程将A股所有的股票都用相同的操作方式以图片的形式展示出来。

实现案例(三一重工)

  1. 股票案例:三一重工(600031)
  2. 无风险收益率:4%
  3. 假设周期:10年
  4. 买入下限:70%
  5. 卖出上限:150%

数据和图表

image

image

实现过程(python代码)

实现环境:

  1. python 3.12
  2. jupyterlab
import akshare as ak
import pandas as pd
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')
#输入股票代码
stock_code = 'SH600031'

#利率,用于计算pe, 公式为pe=1/i_rate
i_rate = 0.04

#买入和卖出的上下线百分比
buy_pct = 0.7
sell_pct = 1.5

#设置移动时间窗口
window = 10

pe = 1 / i_rate
def calc_cape_pd(df, window):  
    trade_dates = pd.to_datetime(df['end_date']).dt.year
    present_mv= df['total_mv']
    reasonable_mv = df['compr_inc_attr_p'].rolling(window).mean()*pe
    upper_band = reasonable_mv*sell_pct
    lower_band = reasonable_mv*buy_pct
    
    calc_df = pd.DataFrame({'日期':trade_dates, '市值':present_mv,'归母净利润':df['compr_inc_attr_p'],'合理市值':reasonable_mv,'卖出市值': upper_band, '买入市值': lower_band})

    return calc_df
# compr_inc_attr_p(PARENT_NETPROFIT):归属于母公司(或股东)的综合收益总额
stock_profit_sheet_by_yearly_em_df = ak.stock_profit_sheet_by_yearly_em(symbol=stock_code)

income = stock_profit_sheet_by_yearly_em_df[['SECUCODE','REPORT_DATE','PARENT_NETPROFIT']]
income = income.rename(columns={'SECUCODE':'ts_code','REPORT_DATE':'end_date','PARENT_NETPROFIT':'compr_inc_attr_p'})

income['end_date'] = pd.to_datetime(income['end_date']).dt.date
income['compr_inc_attr_p'] = income['compr_inc_attr_p'] / 100000000
income = income.sort_values(by='end_date', ascending=True)

#市值数据
mv_ = ak.stock_zh_valuation_baidu(symbol=stock_code[2:], indicator="总市值", period="全部")
mv_ = mv_.rename(columns={'date':'end_date','value':'total_mv'})
#填充missing dates
end_date = income['end_date'].tolist()
missing_dates = {'end_date':end_date}
missing_dates_df = pd.DataFrame(missing_dates)
missing_dates_df['end_date'] = pd.to_datetime(missing_dates_df['end_date']).dt.date

mv = mv_.merge(missing_dates_df, on='end_date', how='outer')
mv.sort_values(by=['end_date'], inplace=True)
mv = mv.fillna(method='bfill')

compr_inc_attr_p_yr = income.merge(mv, on='end_date')
calc_df = calc_cape_pd(compr_inc_attr_p_yr, window)
calc_df
trade_dates = calc_df['日期']
present_mv = calc_df['市值']
upper_band = calc_df['卖出市值']
lower_band = calc_df['买入市值']

plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False #用来正常显示负号
fig, ax = plt.subplots(figsize=(18, 10))
ax.grid()
ax.set_title(f'股票{stock_code} - 周期:{window}年')
ax.set_xlabel('日期', loc='right')
ax.set_ylabel('市值 (亿)', loc='top')
plt.xticks(trade_dates)

## 画出相应的上下限线
ax.plot(trade_dates, upper_band, color='darkgreen', linestyle='-.', label='卖出点位')
ax.plot(trade_dates, present_mv, color='darkgreen', label='对应市值')
ax.plot(trade_dates, lower_band, color='darkgreen', linestyle='--', label='买入点位')          


## 画出相应的上下限线的面积
ax.fill_between(trade_dates, lower_band, upper_band, color='forestgreen',
                label='买入卖出范围', alpha=0.2,
                zorder=2)

ax.legend()
fig.tight_layout()
fig.savefig('cape.pdf')