基于Python3的格雷厄姆 股票估值模型

摘要

在阅读美国经济学家和投资思想家本杰明·格雷厄姆的《聪明投资者》的书籍时,发现有一个成长股的资本化率公式,这个公式是假定上市公司恒定增长,在给定一个合理的市盈率和预期增长率的情况下,预计公司未来7~10年的合理价格。

估值模型思路

获取数据
数据提取
数据清洗
计算估值
存储到数据库

1、获取数据
数据的获取方法有很多,可以直接从同花顺、东方财富网、萝卜投研上获取,这里我是直接从萝卜投研上获取。
2、数据提取
获取数据后,我们从获取的数据中提取需要的财务指标。
3、数据清洗
数据中有一些缺失值,我们使用0进行填充,并保存到MySQL数据库中。
4、计算估值
依据格雷厄姆的成长公式计算股票的估值。
5、存储到数据库
将计算的股票估值存储到MongoDB数据库中。
关于上面第3点,为什么要将清洗后财务数据存储到MySQL数据库中,是因为计算出来的估值仅用于参考,还需要横向对比同行业不同公司的财务指标,选出同行业的龙头企业。

项目结构

如下图所示的项目文件结构
项目文件结构

  • main.py 主文件
  • fetch_stock_data.py 提取数据和清洗数据的模块
  • mysql_drivers.py 在MySQL驱动基础上二次开发的操作函数
  • stockinfo.py 股票的基础数据,例如:股票名称对应的代码
  • valuation_model.py 股票估值模型

模块介绍

fetch_stock_data.py 提取数据模块

数据提取模块包含fillna_value()和fetch_data_one()函数
fillna_value() 用于将异常值 NaN,’-’, np.inf, -np.inf 替换成 -1
fetch_data_one() 函数提出关键的财务数据并返回
代码如下:

def fetch_data_one(self, name):
    ''' 提取某支股票的财务数据

    参数
    ----------
    name : str
       公司名称

    返回值 
    ----------
    DataFrame
        提取的财务数据
    '''

    try:
        # 文件列表
        cw_file = name + r'-财务摘要表.xls'
        fz_file = name + r'-资产负债表.xls'

        # 读取文件
        cw_pd = pd.read_excel(cw_file, index_col=0, header=0)   # 读取 财务摘要表
        fz_pd = pd.read_excel(fz_file, index_col=0, header=0)   # 读取 资产负债表
    except FileNotFoundError:
        print('=== ERROR: 文件 {} 或者 {} 不存在!'.format(cw_file, fz_file))
        # 返回空的DataFrame对象
        return pd.DataFrame()

    print('提取 {} 公司数据'.format(name))
    # 提取关键数据
    cw_pd1 = cw_pd.reindex(['营业收入', '营业收入同比(YOY)', '归属母公司股东的净利润', '归属母公司股东的净利润同比(YOY)',
     '经营活动现金净流量','投资活动产生的现金流量净额', 'ROE(摊薄)', 'ROIC', 'EPS(摊薄)', '股息率', '资产负债率', '折旧与摊销', '销售毛利率', '销售净利润率', 'EBIT', 'EBITDA'])
    fz_pd1 = fz_pd.reindex(['商誉', '非流动资产合计', '负债合计', '货币资金', '应收账款', '存货'])
    concat_data = pd.concat([cw_pd1, fz_pd1], sort=False).T   # 合并图表,并转换行列位置
    self.fillna_value(concat_data)   # 填充缺失值

    # 计算指标
    concat_data['归属母公司股东的净利润 / 经营活动现金净流量'] = concat_data['归属母公司股东的净利润'] / concat_data['经营活动现金净流量']

    # 对列标签重新排序
    finall_data = concat_data.reindex(['营业收入', '营业收入同比(YOY)', '归属母公司股东的净利润', '归属母公司股东的净利润同比(YOY)', '经营活动现金净流量', '归属母公司股东的净利润 / 经营活动现金净流量', '投资活动产生的现金流量净额', '货币基金',
       '应收账款', '存货', '商誉', '折旧与摊销', '非流动资产合计', '资产负债率', '销售毛利率', '销售净利润率', 'EPS(摊薄)', 'ROE(摊薄)', 'ROIC', '股息率', 'EBIT', 'EBITDA'], axis='columns')

    # 对行标签进行排序
    finall_data.sort_index(inplace=True)

    return finall_data

这里我们传入一个公司名称到fetch_data_one()函数,函数会打开对应公司的“财务摘要表”和“资产负债表”Excel文件(我们前面提前下载好,放在工作目录下的),打开文件后,我们提取关键字段,然后将两个DataFrame表对象合并成一个,在处理异常值后返回最终的DataFrame对象。

流程图如下:

打开Excel
提取字段
合并DataFame
行列转置
填充异常值
索引排序
返回DataFrame

mysql_drivers.py 自定义MySQL驱动模块

这个模块自定义了驱动MySQL的函数,包括如下的函数和常量:

SQL_FIELDS 常量:数据库中的变量对照表

MysqlDrivers类说明
init(self, db) 方法
说明:连接数据库

isexists_table(self, table) 方法
说明:查询表是否存在

create_table(self, table) 方法
说明:传入表名table,创建固定结构的表。

query_value(self, table, fields=None)方法
说明:查询表的数据。

insert_values(self, table, params) 方法
说明:插入一组或多组数据

close_connet(self) 方法
说明:关闭打开的数据库连接。

部分代码如下:

# 数据库中的变量对照表
SQL_FIELDS = {
    '公司名称': 'company',
    '报告日期': 'report_date',
    '营业收入': 'taking',
    '营业收入同比': 'taking_growth_rate',
    '净利润': 'retained_profits',
    '净利润同比': 'profits_growth_rate',
    '经营活动现金净流量': 'ONCF',
    '投资活动产生的现金流量净额': 'IONCF',
    '货币基金': 'money_fund',
    '存货': 'inventory',
    '应收账款': 'receivables',
    '商誉': 'goodwill',
    '非流动资产合计': 'Non_Total_current_assets',
    '折旧与摊销': 'depreciation_amortization',
    '负债合计': 'total_liability',
    '资产负债率': 'LEV',
    '销售毛利率': 'gross_profit',
    '销售净利润率': 'sales_profit_margin',
    'EPS': 'EPS',
    '净资产收益率': 'ROE',
    'ROE': 'ROE',
    'ROIC': 'ROIC',
    '股息率': 'DYR',
    '企业主营业务的盈利能力': 'EBIT',
    '企业主营业务产生现金流的能力': 'EBITDA',
    '归属母公司股东的净利润 / 经营活动现金净流量': 'profits_to_cash',
    '非流动资产收益率': 'NCROA',
}

SQL_FIELDS 常量是一个字典,定义了财务指标对应数据库的字段名

def create_table(self, table):
	''' 创建表
	    根据表结构创建数据库的表

	参数
	----------
	table :str
	    表名

	返回值
	----------
	    无
	'''
	# 通用的表结构
	genernal_table = ("""
CREATE TABLE IF NOT EXISTS {} (
company varchar(10) COMMENT '公司名称',
report_date varchar(20) COMMENT '报告日期',
taking decimal(16, 6) COMMENT '营业收入',
taking_growth_rate varchar(10) COMMENT '营业收入同比(YOY)',
retained_profits decimal(16, 6) COMMENT '归属母公司股东的净利润',
profits_growth_rate varchar(10) COMMENT '归属母公司股东的净利润同比(YOY)',
ONCF decimal(16, 6) COMMENT '经营活动现金净流量',
profits_to_cash float COMMENT '归属母公司股东的净利润 / 经营活动现金净流量',
IONCF decimal(16, 6) COMMENT '投资活动产生的现金流量净额',
money_fund decimal(16, 6) COMMENT '货币基金',
receivables decimal(16, 6) COMMENT '应收账款',
inventory decimal(16, 6) COMMENT '存货',
goodwill decimal(16, 6) COMMENT '商誉',
depreciation_amortization decimal(16, 6) COMMENT '折旧与摊销',
Non_Total_current_assets decimal(16, 6) COMMENT '非流动资产合计',
LEV varchar(10) COMMENT '资产负债率',
gross_profit varchar(10) COMMENT '销售毛利率',
sales_profit_margin varchar(10) COMMENT '销售净利润率',
EPS varchar(10) COMMENT 'EPS(摊薄)',
ROE varchar(10) COMMENT 'ROE(摊薄)',
ROIC varchar(10) COMMENT '投资资本回报率',
DYR varchar(10) COMMENT '股息率',
EBIT float COMMENT '企业主营业务的盈利能力',
EBITDA float COMMENT '企业主营业务产生现金流的能力',
update_date date COMMENT '当前的时间',
primary key (company, report_date)
)
	""")
	# 创建表 table
	# print(type((table)))
	self.cursor.execute(genernal_table.format(table))
	self.cnx.commit()   # 确定修改

create_table()函数创建一个固定结构的表,表的名称以股票代码来命名,例如:深圳主板上市的格力电器,代码:000651,则表的名称为sz_000651
股票名称对对应表的名称存在于 stockinfo.py 模块的STOCK_INFO 常量中。

def query_value(self, table, fields=None):
    ''' 查询表数据
        查询表table字段fields的数据;如果fields为None,则查询表table所有数据

    参数
    ----------
    table :str
        表名
    fields :str,list,tuple
        查询的字段,默认为None,查询所有字段的数据

    返回值
    ----------
    list
        查询到的数据
    '''
    # print(fields)
    if fields is None:   # 如果fields为None,则查询所有数据
        query = ("SELECT * FROM {}".format(STOCK_INFO[table]))
    elif isinstance(fields, (tuple, list)):  # 如果fields是列表或者元组, 则查询多个字段
        fields1 = ','.join(
    [SQL_FIELDS[key] for col in fields for key in SQL_FIELDS.keys(
    ) if re.match('.*{}.*'.format(col), key)])    # 拼接查询内容
        query = ("SELECT {} FROM {}".format(fields1, STOCK_INFO[table]))
    else:  # 查询一个字段
        field = [SQL_FIELDS[key] for key in SQL_FIELDS.keys(
        ) if re.match('.*{}.*'.format(fields), key)][0]
        query = ("SELECT {} FROM {}".format(field, STOCK_INFO[table]))

    self.cursor.execute(query)

    return self.cursor.fetchall()

query_value() 函数查询数据库的内容,默认查询某个表的所有字段。
流程图如下:

None
tuple or list
其余情况
fields
查询所有字段
查询tuple or list中的字段
查询一个字段
返回查询值

valuation_model.py 股票估值模块

这个模块计算股票的估值,例如:PE、PB、内生增长率等。主要包括如下的函数:
to_float(self, string) 函数
说明:将带%的字符串转换成浮点型数值

correction_growth(self, growth) 函数
说明:修正净利润同比增长率

collection_struct(self, *args) 函数
说明:转换成MongoDB 数据结构

calculate_value(self, name, table) 函数
说明:计算企业价值,包括PE, PB, 股价,预期股价,净利润同比增长率,内生增长率

股票的估值指标主要有PE、PB、PS等
市盈率(PE)是指股票价格除以每股收益(每股收益,EPS)的比率
市净率(PB)指的是每股股价与每股净资产的比率
PE(市盈率)、PB(市净率)的计算方法参照雪球大V的 坤鹏论:用ROE推算合理的PE和PB 用盈再率看企业投资效率 文章提到的公式进行计算,
它的方法考虑了ROE(净资产收益率)和股息支付率,更具有参考意义。
公式如下:
PE=(1-股利支付率)×ROE×100+股利支付率×10
PB=((ROE^2)×(1-股利支付率))×100+ROE×股利支付率×10

优秀的企业都应该是持续成长的,这也就是巴菲特的“具有可持续竞争优势”。
自我维持增长率是衡量公司在不增加外部权益时最大的增长能力的指标,它也叫企业内生收益增长率,或是所有者权益增长率。
计算方法参照雪球大V的坤鹏论:自强才是真强!企业的自我维持增长率怎么算?
公式如下:
自我维持增长率=(1-股利支付率)×权益回报率(ROE)

格雷厄姆的《聪明投资者 第四版》著作中第七章中记载的成长股的资本化率公式:在这里插入图片描述
当期利润是每股盈利EPS,预期年增长率不需要百分号(如:20%就是20),8.5是认为一家成长型公司的合理市盈率。
例如:某股票最新的EPS为1.0,预期年增长率为20%
那么现在的 合理股价 = 1.0 x (8.5 + 2 x 20) = 48.5元

部分代码如下:

def calculate_value(self, name, table):
    ''' 计算企业价值
        包括PE, PB, 股价,预期股价,净利润同比增长率,内生增长率

    参数
    ----------
    name :str
        公司名称
    table : PrettyTable object
        漂亮输出的对象

    返回值 
    ----------
    dict
        企业估值集合
    '''
    report = self.mysql.query_value(name, '报告日期')  # 报告日期列表
    # 如果最新的为季度报数据,则使用上一年年报数据
    N = -1 if re.match('.*季报', report[-1][0]) else 0

    report_date = report[-1 + N][0]  # 报告日期
    roe = np.average([self.to_float(roe[0])
            for roe in self.mysql.query_value(name, 'ROE')][-6 + N: -1 + N])  # 近5年平均ROE
    roic = np.average([self.to_float(roic[0])
            for roic in self.mysql.query_value(name, 'ROIC')][-6 + N: -1 + N])  # 近5年平均ROIC
    growth = np.average([self.to_float(roe[0])
                for roe in self.mysql.query_value(name, '净利润同比')][-6 + N: -1 + N])  # 近5年平均净利润同比增长率
    eps = float(self.mysql.query_value(name, 'EPS')[-1 + N][0])  # EPS

    payouts = PAYOUTS_INFO[name]  # 股利支付率

    # 合理PE = (1 - 股利支付率) * ROE * 100 + 股利支付率 * 100  ; 公式来源于:雪球——坤鹏论
    pe = (1 - payouts) * roe * 100 + payouts * 10

    # 合理PB = (ROE)^2 * (1 - 股利支付率) * 100 + ROE * 股利支付率 * 10  ; 公式来源于:雪球——坤鹏论
    pb = np.power(roe, 2) * (1 - payouts) * 100 + roe * payouts * 10

    # 自我维持增长率 = (1 - 股利支付率) * ROE  ; 公式来源于:雪球——坤鹏论
    self_growth = (1 - payouts) * roe

    # 格雷厄姆公式合理股价 = EPS * (PE + 2 * G),对应于复利公式:EPS * PE * (1+G)^10;股价 = EPS * PE
    if re.match('.*银行', name):
        graham = eps * (pe + 2 * self.correction_growth(growth)
                * 0.8 * 100)  # 银行板块,预期增长率G打8折
    else:
        graham = eps * (pe + 2 * self.correction_growth(growth)
                * 0.5 * 100)  # 其余板块,预期增长率G打5折
    
    # 如果ROIC不为-1,则格式化roic
    roic_1 = '{:.2f} %'.format(roic * 100) if roic != -1 else roic 
    # 添加输出的数据 ['公司名称', '报告日期', EPS, 'ROE', 'ROIC', PE', 'PB', '合理股价','预期股价',  '净利润同比增长率', '内生增长率']
    table.add_row([name, report_date, eps, '{:.2f} %'.format(roe * 100), roic_1, pe, pb, 
            pe * eps, graham, '{:.2f} %'.format(growth * 100), '{:.2f} %'.format(self_growth * 100)])
    # 转换成 MongoDB 数据结构
    coll = self.collection_struct(name, report_date, eps,
                pe, pb, graham, roe, roic, growth, self_growth, payouts)

    return coll

calculate_value()函数传入公司的名称,然后从数据库中提取公司的财务数据用于计算股票估值。

def correction_growth(self, growth):
    ''' 修正净利润同比增长率
    平均增长率 >= 40%, 修正为 40%
    平均增长率 >= 30%, 修正为 原来的80%
    平均增长率 < 0%, 修正为 0%
    其余情况,平均增增长率保持不变

    参数
    ----------
    growth :float
        平均增长率

    返回值 
    ----------
    float
        修正后净利润同比增长率
    '''
    if growth >= 0.4:  # 平均增长率 >= 40%
        return 0.4
    elif growth >= 0.3:  # 平均增长率 >= 30%
        return growth * 0.8
    elif growth < 0:  # 平均增长率 < 0%
        return 0
    else:
        return growth  

correction_growth()函数修正平均净利润增增长率。对于一家公司来说,保持5年以上保持40%的同比增长率的可能性是很小的,因此对于平均增长率大于40%的改为40%;对于大于30%的,我们打8折;对于增长率为负数的,认为不增长;其余情况保持不变。
流程图如下:

>=0.4
>=0.3 且 <0.4
<0.3
<0
growth
growth=0.4
gowth*0.8
growth
growth=0
修正growth

这个项目的运行结果如下:
为了避免推荐股票的嫌疑,我将公司名称用xxxx来替代。

x:\test_python\财务报表
+----------+----------+------+---------+---------+-------+------+----------+--------------+------------------+------------+
| 公司名称 | 报告日期 | EPS  |   ROE   |   ROIC  |   PE  |  PB  | 合理股价 | 预期10年股价 | 净利润同比增长率 | 内生增长率 |
+----------+----------+------+---------+---------+-------+------+----------+--------------+------------------+------------+
| xxxxxxx | 2018年报 | 4.36 | 30.55 % | 22.12 % | 24.38 | 7.45 |  106.31  |    223.43    |     26.86 %      |  21.38 %   |
| xxxxxxx | 2018年报 | 3.05 | 23.22 % | 16.91 % | 17.67 | 4.10 |  53.88   |    175.88    |     42.98 %      |  13.47 %   |
| xxxxxxx | 2019年报 | 6.34 | 16.52 % | 14.92 % | 14.56 | 2.40 |  92.32   |    267.77    |     34.59 %      |  11.56 %   |
| xxxxxxx | 2018年报 | 2.20 | 12.42 % | 11.84 % | 11.69 | 1.45 |  25.73   |    66.77     |     18.65 %      |   8.69 %   |
| xxxxxxx | 2018年报 | 0.66 | 15.05 % | 14.75 % | 10.51 | 1.58 |   6.93   |    18.04     |     16.83 %      |   1.51 %   |
+----------+----------+------+---------+---------+-------+------+----------+--------------+------------------+------------+
写入 MongoDB 数据库完成!

PS x:\程序开发\应用案例\股票估值案例>

以上我只是选取了部分核心的模块和函数进行说明,其余的可以直接看程序的注释,如有不懂的欢迎评论,如有侵权联系立删。

以上转载内容如下:

  1. 雪球作者:坤鹏论 链接:https://xueqiu.com/5992486099/140360538

  2. 雪球作者:坤鹏论
    链接:https://mp.weixin.qq.com/s?__biz=MzIyMzE3MzgxMw==&mid=2650996055&idx=1&sn=9fb5ef869e517c54d9be6011264135e5&chksm=f3d4108dc4a3999bdc181bd13e9d294a5c33fea440e079f65ad815d4692afa0bdadd3ee08467&scene=21#wechat_redirect

  3. 《聪明的投资者(第四版)》第七章

  • 8
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 26
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 26
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值