Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
chinobing opened this issue Dec 21, 2024 · 0 comments
Open

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

chinobing opened this issue Dec 21, 2024 · 0 comments
Labels

Comments

@chinobing
Copy link
Owner

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')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant