使用Python构建股票财务指标打分系统
最近受到知识星球圈友【禄子₇】的启发,利用Python基于财务指标打分构建价值投机股票的选股系统。首先感谢他给我发的电子书《价值投机》和他自己写的code(公众号回复“价值投资”或‘210319’即可获取)。书中前半部分阐述了财务指标的含义及打分模型,后半部分则介绍了技术分析和投资策略。本文以第一部分价值分析和评分模型为基础,使用Python构建一个价值投机评分选股系统。实际上这里的打分模型本质上是一个多因子模型,书中列出的十项财务指标相当于“因子”,而因子权重主要根据经验分析进行赋值,值的范围在0-10,最后汇总得分。下面带领大家一起了解一下相关财务指标的构成及其打分模型,并使用Python实现选股系统的构建。
营业收入增长率是衡量企业经营状况和市场占有能力、预测企业经营发展趋势、评价企业成长状况的重要指标。一般而言,当营业收入增长率大于10%时,表明公司产品处于成长期;当营业收入增长率在5%-10%时,说明公司产品已经进入稳定期,不就将进入衰退期;如果该比率低于5%,表明公司产品可能逐渐进入衰退期。从基本面分析的角度看,我们要寻找那些处于上升期、成长期的公司,因此在量化的时候以营业收入增长率达到10%作为基准进行评分,以最近季度增长率与10%进行对比,低于10%则每减少1%减1分(四舍五入整数),直至0分;高于10%则每增加1%加1分,最多至10分。
一般而言,公司盈利能力越强,投资价值越高。书中将将选择标准定在20%,即以增长20%为基准计5分,以最近季度增长率与20%进行对比,低于20%则每减少2%减1分,直至0分;高于20%则每增加2%加1分,最多至10分。
毛利率是毛利与销售收入或营业收入的百分比,即毛利率=(营业收入-营业成本)/营业收入。毛利率是公司的重要经营指标,能反映公司产品的竞争力和获利潜力,该指标可以反映公司某一主要产品或主要业务的盈利状况。与历史相比,若公司的毛利率显著提高,则可能是公司所在行业处于复苏时期,产品价格显著上升。反之,若公司毛利率显著降低,则可能是公司所在行业竞争激烈,毛利率下降往往伴随着价格战的爆发或成本的失控,意味着盈利能力的下降。当然,行业不同毛利率差异很大,因此毛利率只适合纵向比较。以前三年度平均毛利率为基准计5分,以最近季度毛利率与前三年度平均毛利率比较,低于前三年平均毛利率则每减少0.5%减1分,直至0分;高于近三年平均毛利率则每增加0.5%加1分,最多10分。
期间费用率=(营业费用+管理费用+财务费用)/营业收入,该指标能反映公司的费用规模、管控能力、运营水平,是评估公司经营管理能力和效率的重要指标。由于行业不同,期间费用率差异较大,因此评价期间费用率指标,只能是公司现在的期间费用率水平与过去年度进行比较,查看期间费用率是在提升还是下降。以前三年度平均期间费用率为基准计5分;以最近季度期间费用率与前三年度平均期间费用率进行对比,高于前三年度平均期间费用率则每增加0.5%减1分,直至0分;低于前三年度平均期间费用率则每减少0.5%加1分,最多至10分。
存货周转率用于反映存货的周转速度,可以反映企业存货管理水平的高低,影响企业的短期偿债能力。由于不同行业该指标差异较大,因而只适合自身纵向比较,即观察存货周转率是在提升还是下降。评分以前三年度平均存货周转率为基准计5分,以最近季度的年化存货周转率与前三年度平均存货周转率进行对比,每提高2%加1分,最多至10分,每降低2%减1分,直至0分。即存货周转率分值=5+(最近季度的年化存货周转率-前三年度平均存货周转率)/前三年度平均存货周转率x100/2。
每股经营现金流量反映公司为每一普通股获取的现金流入量,计算公式为(公司经营活动现金流入-经营活动现金流出)/总股本,常用来衡量公司的盈利质量,如果每股收益远高于每股经营现金流量,说明公司当期销售形成的利润多为账面利润,没有在当期为格式带来真金白银,即利润或每股收益质量差,可能是虚假繁荣。一般而言,每股经营性现金流量越大越好,但当公司在高速扩张期时期每股经营性现金流量可能为负。当然如果一家公司连续出现每股现金流量为负值是值得警惕的,潜在风险包括过度扩张、应收账款不能及时收回产生坏账风险,财务造假、虚增利润等。评分标准:以最近三年每股收益之和为基准计5分,以最近三年每股经营性现金流量之和与最近三年每股收益之和对比,每增加4%加1分,,最多10分;每减少4%减1分,直至0分。
加权净资产收益率指标是一个综合性很强的指标,是衡量公司盈利能力、股东资金使用效率的重要指标。加权净资产收益率指标值越高,说明自有资本获得净收益的能力越强。一般而言,一家优秀的成长型公司的加权净资产收益率应保持在每年15%以上。因此评分以15%为基准计5分,以最近季度的年化加权净资产收益率与15%进行对比,每增加1%加1分,最多10分;每减少1%减1分,直至0分。
总资产报酬率表示企业全部资产获取收益的水平,能较全面地反映企业的获利能力和投入产出状况。一般情况,企业可根据此指标与市场资本利率进行比较,若该指标大于市场利率,则表明企业可以利用财务杠杆进行负债经营,获取更多的收益,该指标越高,表明企业投入产出水平越好,企业的资产运营越有效。评分标准为:以5%为基准计0分,以最近季度年化总资产报酬率(最近季度为四季度则为最近年度总资产报酬率)与5%进行对比,每增加0.5%加1分,最多10分;每降低0.5%减1分,直至0分。其中最近季度的年化总资产报酬率=上年度总资产报酬率X最近季度总资产报酬率/上一年同季度总资产报酬率。即(最近季度年化总资产报酬率-5)/0.5。
市净率是每股股价与每股净资产的比率,一般而言,市净率越低的股票其投资价值越高;反之,市净率越高的股票,其投资价值越小。历史经验表明,一些跌破净资产的股票往往会表现为较小的投资风险,而在行情启动时,具有较好的价值回归动力。以3倍市净率为基准计5分,每降低0.4倍加1分,最多10分,每提高0.4倍减1分,如果确定以市净率为主要估值指标,则市净率分值无下限,如果确定以PEG(动态市盈率相对盈利增长比率)指标为估值指标,则市净率分值最低0分。
PEG等于公司的市盈率除以公司的盈利增长速度,与市净率一样,PEG是一个估值指标,两者在使用时侧重点不同。一般而言,PEG值越低,股价被低估的可能性越大,相应的投资价值就可能越大;相反,PEG值越高,股价被低估的可能性越小,相应的投资价值就可能越小。
PEG=动态市盈率/过去三年平均净利润增长率,其中动态市盈率=现在股价/年化每股收益;最近季度的年化每股收益=上年度每股收益X最近季度每股收益/上一年度同季度每股收益。评分标准:以PEG值等于1为基准计5分,每降低0.1倍加1分,最多10分;每提高0.1倍减1分。如果确定以PEG为主要估值指标,则PEG分值无下限,如果确定以市盈率为主要估值指标,则PEG分值最低0分。得分=5-[(动态市盈率相对盈利增长比率-1)/0.1]。
将上述指标计分汇总得到总分,理论上当总分大于50分认为具备投资价值,因为选定的十项指标中的每一项指标的中间值都是按照具备投资价值的公司标准选定的。上述指标对于判断非周期性行业股票的投资价值,以及高成长性股票的估值是否合理、成长性能否持续都具有重要意义;对于周期性行业的股票也适用,但周期性股票的买卖点主要看行业的周期性拐点,分值高低反而不重要。值得注意的是,上述十项指标评估模型理论上可以适用于任何生产销售型公司,但金融行业(银行、券商等)不能直接适用,有些指标需要重新设定。
下面使用Python实现上述指标的计算、评分、可视化和选股。首先引入后面需要用到的包,本文使用tushare获取公司财务指标和股价数据。注意某些数据可能存在积分权限,token是注册该网站获取。
import pandas as pd
import numpy as np
from datetime import datetime
#画图
import matplotlib.pyplot as plt
%matplotlib inline
from pylab import mpl
mpl.rcParams['font.sans-serif']=['SimHei']
mpl.rcParams['axes.unicode_minus']=False
#获取数据
import tushare as ts
token='输入你在tushare上获取的token'
ts.set_token(token)
pro=ts.pro_api(token)
需要用到的财务指标名称及其在tushare上的调用函数:
{'营业收入增长率':'tr_yoy','营业利润增长率':'op_yoy','毛利率':'grossprofit_margin','期间费用率':'expense_of_sales','存货周转率':'inv_turn','每股经营性现金流量':'ocfps', '每股收益':'eps','加权净资产收益率':'roe_waa','年化净资产收益率':'roe_yearly', '年化总资产报酬率':'roa2_yearly','归属母公司股东的净利润-扣除非经常损益同比增长率':'dt_netprofit_yoy'}
编写函数获取相应的财务指标,输出dataframe格式。
def get_indicators(code):
#获取当前时间,计算当前和过去四年时间
t0=datetime.now()
t1=datetime(t0.year-4,t0.month,t0.day)
end=t0.strftime('%Y%m%d')
start=t1.strftime('%Y%m%d')
#财务比率
fields='ann_date,end_date,tr_yoy,op_yoy,\
grossprofit_margin,expense_of_sales,inv_turn,eps,\
ocfps,roe_yearly,roa2_yearly,netprofit_yoy'
fina = (pro.fina_indicator(ts_code=code,start_date=start, end_date=end,fields=fields)
.drop_duplicates(subset=['ann_date','end_date'],keep='first'))
fina.set_index('end_date',inplace = True)
fina=fina.sort_index()
#获取市盈率和市净率指标(pe、pb数据)
pbe=pro.daily_basic(ts_code=code, fields='trade_date,pe_ttm,pb')
pbe.set_index('trade_date',inplace=True)
pbe=pbe.sort_index()
#合并数据
df=pd.merge(fina,pbe,left_index=True,right_index=True,how='left')
#pb缺失数据使用前值填充,pe不管,缺失值可能是因为盈利为负数
df['pb'].fillna(method='ffill',inplace=True)
return df
编写各项财务指标的评分函数。
#存在缺失值或者负值(市盈率)情况下评分直接为0
#营业收入增长率打分(0-10)
def cal_tryoy(y):
'''y是营业收入增长率'''
try:
return 5+ min(round(y-10),5) if y>=10 else 5+ max(round(y-10),-5)
except:
return 0
#营业利润增长率打分(0-10)
def cal_opyoy(y):
'''y是营业利润增长率'''
try:
return 5+ min(round((y-20)/2),5) if y>=20 else 5+ max(round((y-20)/2),-5)
except:
return 0
#毛利率打分
def cal_gpm(y):
'''y是最近季度毛利率-前三季度平均毛利率'''
try:
return 5+min(round(y)/0.5,5) if y>0 else max(round(y)/0.5,-5)+5
except:
return 0
#期间费用率打分
def cal_exp(y):
'''y是最近季度期间费用率-前三季度平均期间费用率'''
try:
return 5+min(round(y)/0.5,5) if y>0 else max(round(y)/0.5,-5)+5
except:
return 0
#存货周转率打分
def cal_inv(y):
'''y是(最近季度存货周转率-前三季度平均存货周转率)/前三季度平均存货周转率*100'''
try:
return 5+min(round(y/2),5) if y>0 else max(round(y/2),-5)+5
except:
return 0
#每股经营性现金流打分
def cal_ocfp(y):
'''y是(最近三季度每股经营性现金流之和-最近三季度每股收益之和)/最近三季度每股收益之和*100'''
try:
return 5+min(round(y/4),5) if y>0 else max(round(y/4),-5)+5
except:
return 0
#净资产收益率打分
def cal_roe(y):
'''y是年化净资产收益率'''
try:
return 5+ min(round(y-15),5) if y>=15 else 5+ max(round(y-15),-5)
except:
return 0
#总资产报酬率打分
def cal_roa(y):
'''y是最近季度年化总资产报酬率'''
try:
return min(round((y-5)/0.5),10) if y>=5 else max(round(y-5),0)
except:
return 0
#市净率打分
def cal_pb(y):
'''y是市净率'''
try:
return 5-max(round((y-3)/0.4),-5) if y<=3 else 5-min(round((y-3)/0.4),5)
except:
return 0
#动态市盈率相对盈利增长率(PEG)打分
def cal_pe(y):
'''y是动态市盈率相对盈利增长率'''
try:
return 5-max(round((y-1)/0.1),-5) if y<=1 else 5-min(round((y-1)/0.1),5)
except:
return 0
计算各项财务指标得分。
#计算财务指标得分
def indicator_score(code):
data=get_indicators(code)
'''(1)营业收入增长率打分'''
data['营收得分']=data['tr_yoy'].apply(cal_tryoy)
'''(2)营业利润增长率打分'''
data['利润得分']=data['op_yoy'].apply(cal_opyoy)
'''(3)毛利率打分'''
#计算最近季度毛利率-前三季度平均毛利率
data['gpm']=data['grossprofit_margin']-data['grossprofit_margin'].rolling(3).mean()
data['毛利得分']=data['gpm'].apply(cal_gpm)
'''(4)期间费用率打分'''
#最近季度期间费用率-前三季度平均期间费用率
data['exp']=data['expense_of_sales']-data['expense_of_sales'].rolling(3).mean()
data['费用得分']=data['exp'].apply(cal_exp)
'''(5)周转率打分'''
#(最近季度存货周转率-前三季度平均存货周转率)/前三季度平均存货周转率*100
data['inv']=(data['inv_turn']-data['inv_turn'].rolling(3).mean())*100/data['inv_turn'].rolling(3).mean()
data['周转得分']=data['inv'].apply(cal_inv)
'''(6)每股经营现金流打分'''
#(最近三季度每股经营性现金流之和-最近三季度每股收益之和)/最近三季度每股收益之和*100
data['ocf']=(data['ocfps'].rolling(3).sum()-data['eps'].rolling(3).sum())*100/data['eps'].rolling(3).sum()
data['现金得分']=data['ocf'].apply(cal_ocfp)
'''(7)净资产收益率打分'''
data['净资产得分']=data['roe_yearly'].apply(cal_roe)
'''(8)总资产收益率打分'''
data['总资产得分']=data['roa2_yearly'].apply(cal_roa)
'''(9)市净率打分'''
data['市净率得分']=data['pb'].apply(cal_pb)
'''(10)动态市盈率相对盈利增长率打分'''
#动态市盈率相对盈利增长率
data['peg']=data['pe_ttm']/data['netprofit_yoy'].rolling(3).mean()
data['市盈率得分']=data['peg'].apply(cal_pe)
#计算总得分
data['总分']=data[['营收得分','利润得分','费用得分','周转得分','现金得分','净资产得分','总资产得分',\
'市净率得分','市盈率得分']].sum(axis=1)
return data[['营收得分','利润得分','费用得分','周转得分','现金得分','净资产得分','总资产得分',\
'市净率得分','市盈率得分','总分']]
以贵州茅台为例,查看其财务指标各季度得分和汇总情况。
#贵州茅台
code='600519.SH'
indicator_score(code)
根据书中的打分模型,贵州茅台这只大牛股的得分并没有特别突出。
当个股指标总得分高于50模型认为具有投资价值,下面对该信号进行可视化,可视化函数代码较长此处略(见知识星球“Python金融量化”)。
plot_signal(code)
plot_stock_signal(code)
从打分模型中给出的交易信号看,该模型对贵州茅台个股的指导作用较差,买入持有反而是更理想的策略。当然,正如书中提到,当前财务指标打分高于标准并不代表可以立刻买入,还要结合个股的技术形态进一步确认。
获取A股上市交易股票进行横向比较
下面获取A股当前交易的股票,分别剔除上市时间短于4年个股(时间短财务指标无法反映趋势)、银行券商等金融股(指标模型不适用)、ST和*ST股。
#获取当前正常上市交易的股票列表
def get_code():
t=datetime.now()
df=pro.stock_basic(exchange='', list_status='L', fields='ts_code,symbol,name,area,industry,list_date')
#排除上市日期短于4年的个股
#获取当前年份
year=datetime.now().strftime('%Y')
#四年前
year=str(int(year)-4)+'0101'
#保留上市时间大于四年个股数据
df=df[df.list_date<year]
#排除银行、保险、多元金融公司
df=df[-df.industry.isin(['银行','保险','多元金融'])]
#排除st和*ST股
df=df[-df.name.str.startswith(('ST'))]
df=df[-df.name.str.startswith(('*'))]
code=df.ts_code.values
name=df.name
return dict(zip(name,code))
计算股票财务指标总分并合并成一个dataframe
#计算所有股票财务指标总分
def get_all_score():
df=pd.DataFrame()
for name,code in get_code().items():
try:
df[name]=indicator_score(code)['总分']
except:
continue
return df
dff=get_all_score()
获取最近财务报表(年报还未出)总分排名前十五只个股。
#获取最近日期总分排名前15个股
dff.T.sort_values(dff.T.columns[-1],ascending=False).iloc[:15,-10:]
plot_stock_signal(get_code()['电魂网络'])
本文根据《价值投机》书中的十项财务指标及其打分模型,分享了使用Python构建基于财务指标的选股系统。文中提及股票仅供学习交流,不构成任何投资建议。实际上上述打分模型存在一定的局限性,缺乏对策略的历史回测和模型评估,其适用性和范围有待读者进一步完善。根据传统股票定价模型,股票价值应等于公司未来收入现金流的贴现值。但是现实中往往很难使用股票定价模型对股票价格作出准确判断,首先,由于公司未来收入的现金流很难刻画和预测,因此股票价值本身是很难精确评估的,只能做到模糊的估测;其次,股票交易价格受到交易情绪(如投机氛围)和尾部风险(极端事件)的影响比较大,因此短期上看股价波动一般比公司基本面的波动要大;再次,股价是实时变动的,反映了投资者的预期,而财务信息存在一定的滞后性,包括财务报表的披露与报告日期存在较长时间差,可能带来未来函数;最后,国内很多上市公司的财务信息可靠性较差。尽管如此,量化投资应该多与基本面结合,寻找符合经济含义解释(理论)的因子比单纯依靠数据挖掘更有可靠性。
资料来源:
海洋. 2017. 《价值投机——投资标准化之路》.中国经济出版社
PS:公号内回复「Python」即可进入Python 新手学习交流群,一起 100 天计划!
老规矩,兄弟们还记得么,右下角的 “在看” 点一下,如果感觉文章内容不错的话,记得分享朋友圈让更多的人知道!
【神秘礼包获取方式】