浅谈估值模型 (三): 回报率r的进阶玩法——Fama-French及PSM(Pastor Stambaugh Model)

摘要及声明

1:本文主要介绍回报率的计算方法,详细解释Fama-French模型及PSM(Pastor Stambaugh Model),并以A股某上市公司为例简单实现一个PSM,最后笔者对多因子存在的问题及壳资源污染进行讨论; 

2:本文主要为理念的讲解,模型也是笔者自建,文中假设与观点是基于笔者对模型及数据的一孔之见,若有不同见解欢迎随时留言交流;

3:笔者希望搭建出一套交易体系,原则是只做干货的分享。后续将更新更多内容,但工作学习之余的闲暇时间有限,更新速度慢还请谅解;

4:本文主要数据通过Tushare(ID:444829)金融大数据平台接口获取,部分数据通过爬虫获取;

5:本文模型采用了多线程技术辅助加速;

6:模型实现基于python3.8;

        前段时间有读者私信笔者的发文的顺序有些混乱,先解释一下。笔者写文章更多是分享笔者自己所学所闻的感悟与心得体会,一则帮助自己整理逻辑;二则也是个与同行们交流的窗口;三则希望文章可以帮助在学习中遇到困惑的朋友。既然是感悟与心得体会嘛,自然是比较随性。有时候突然有了什么灵感,临时起意发一篇,因此发文顺序有些混乱。不过形散意不散,笔者发文的框架依旧是基于体系的搭建,原则依旧是基于干货的分享。特将之前的主要文章总结如下,笔者认为框架感还是很强的:

往期速览
系列 子类别 文章传送门 实现方式
基本面分析 绝对估值 实现GGM的理想国 Python
折现率——PSM模型(本文) Python
相对估值 PE指标平滑 Python
PE Band Python
技术分析 / 蒙特卡洛模拟 Python
/ 全连接神经网络模型 Python
财务分析 财务建模 利润表 R
金融数据获取 / 多线程爬取 Python
/ 多进程爬取 Python
/

selenium模拟网页爬虫

Python

        本期主要针对现金流折现计算中最重要的回报率进行讨论, 主要内容如下,前面老样子还是理念的讲解,不想看的读者可以跳过直接进第六部分代码实现:

目录

1. 折现的重要性

2. 折现率VS回报率

3. CAPM的陷阱

3.1 理想的假设

3.2. 理想的回报

4. 为什么要自建折现率模型

5. 折现率有多少进阶玩法

5.1 Fama-French三因子

5.2 PSM

6.代码实现

6.1 思路

6.2 数据准备

6.3 RMRF因子

6.4 SMB因子, HML因子及LIQ因子

6.5 敏感系数

6.6 回归分析

7. 因子有效性讨论

8. 笔者的话


1. 折现的重要性

        折现率,又写做r,复利的威力笔者就不说了,很多人认为是世界第八大奇迹。对于金融市场而言,如果公司是船,市场是海,水涨则船高,那么这个r就好比是波塞冬的三叉戟,操控着整个海洋的水位。网上关于r的文章一大堆,但万变不离其宗,无非是资产的回报,货币时间价值,但有很多投资者不重视这个r,转而关注景气度,更有甚者一些投顾还把高景气度看做是安全垫。

        在笔者看来r是比增速更重要的存在,因为r是基础,是大环境。大环境不好往往先死掉的就是高景气方向,因为高景气度增速高,产生的现金流也在未来的远期,这些远端现金流被r折现个N期后就不剩多少了。反而是那些近端现金流稳定,增速不是很高的价值类公司容易在大环境狂风暴雨时走出来。

2. 折现率VS回报率

        本期文章名字本来是"折现率r", 笔者在斟酌了一下还是改成了回报率。 笔者在这里解释一下它们之间的区别,免得一下用折现率一下又说成回报率误人子弟,因为这两个概念大多数情况下虽然可以划等号,但却是有不同金融意义的,因为本文所讨论的更多是计算回报率,而不是折现率。

        笔者提一个最重要的区别:折现率是成本概念,回报率是收益概念。这不需要太多解释吧,字面意思就能感觉出来。既然一个是成本,一个是收益,站在投资者角度上看自然是希望折现率<收益率;站在企业角度上看则希望折现率>收益率;在公式中两种叫法都可以的原因是我们计算的是内在价值,即市场公允定价状态下不亏也不赚的状态,那折现率与回报率就相等了

        笔者还要再谈一个点,很多文章基本不区分折现率和回报率两个概念,例如DFCF折现(公式一):

 Intrinic value=\sum \frac{CF_{i}}{(1+r_{i})^{n}}\, \, \, \, \,[1]

        在这个公式里r即是折现率,可以看作是利率,又可以看做是回报率,还可以看作是投资的机会成本,甚至把它当作IRR内含回报率(前提是当前价格与内在价值相等)。笔者看来”r“还是那个”r“,数字还是那个数字,但所衍生出来的这些概念在不同的资产上所蕴含的金融意义却是不同的,在表述的过程中需要注意体现专业性。

3. CAPM的陷阱

        计算预期回报率的方式有非常多,大名鼎鼎的CAPM(资本资产定价模型)笔者就不用过多介绍了吧,在最早一期的GGM模型(浅谈估值模型(一)实现GGM的理想国)中笔者就用了这个模型计算预期回报率。那篇文章主要是讨论GGM,更重要的是如何让GGM更有效的理念讲解,如果把折现率的东西插进去文章就太长了,因此简单使用了CAPM计算。网上有一大堆模型和文章用的都是CAPM,但笔者在实务中从来不用CAPM计算预期回报率,原因很简单,CAPM太过理想化

3.1 理想的假设

        CAPM模型有几个重要的假设:1)资产无限可分;2)无风险利率借贷; 3)没有税收和交易费用;4)市场信息信息对称;5)投资者风险厌恶,效用最大化;6)所有投资者对资产有相同预期

        除了这些假设,不知道有多少人注意到CAPM模型里的市场回报率,往往我们使用大盘指数计算市场回报,实际上这也有问题。CAPM市场回报指的是全球资产回报,也就是说理论上要计算市场回报应该把全世界所有国家可投资的所有资产放到一个投资组合里,这个投资组合的回报才算是市场回报。但这样做代价太大了,于是某个大盘指数就被当作备胎放了进去。

        假设是模型使用的前提条件,脱离了假设模型很大程度上将会失效,正因为如此强大的假设条件,CAPM更多是在理论研究方面发光发热。对实务而言,试问面对如此苛刻的几条假设和条件你还敢用CAPM吗

3.2. 理想的回报

        第二个陷阱在于回报率r,在CAPM假设条件加持下,这个r除了是预期的r,还是排除了非系统性风险后的预期r。首先,CAPM所指的r是预期回报率E(r),而非真实回报率r。可别小看这两字只差,预期不代表真实,预期可能在天上,真实可能在地上。其次是只反映系统性风险,其原因是金融学认为承担非系统性风险是不给回报补偿的(Markowitz有效前沿理论,但这是另一个故事了,以后写到组合管理在说这个,简单来讲就是在有效市场下,所有的投资者都可以通过投资组合分散非系统性风险,既然大家都可以消除非系统性风险,因此承担非系统性风险就没有回报了)。但笔者认为看个股很多时候还是需要考虑非系统性风险的,如果不考虑非系统性风险,那分析商业模式,分析财务报表,分析股权结构,分析管理层岂不都是分析个寂寞?

         总而言之CAPM是象牙塔下的理想模型,对实务帮助非常有限。

4. 为什么要自建折现率模型

        事实上有很多途径可以直接或间接获取证券的回报率,网上有很多别人已经算好的结果。像Choice,Wind这些金融终端连r都不用算,输参数进去直接把DDM折现的结果算出来了。

浅谈估值模型 (三): 回报率r的进阶玩法——Fama-French及PSM(Pastor Stambaugh Model)_第1张图片

图一:Choice金融终端DDM计算工具

        这些计算器功能还是很全的,还支持多阶段折现,不同情景下的敏感性分析。但从输入的参数中不难看出所都是用的CAPM模型。事实上所有市面上能公开看到的预期收益率计算统统用的是CAPM,尽管CAPM有如此强的假设条件,大家还是硬着头皮上。原因很简单,CAPM广受认可,使用门槛低,参数设置也不是那么主观。反观其它模型,不仅复杂,而且其中变量和参数设置如果不正确,模型很容易会面临失效,而这些参数的选择和调整都是需要一定专业判断的技术活。如果哪天你开发了个厉害的模型,但是却要设置很多超参数,那么这个模型注定有很高的门槛并很难被大(散)众(户)广泛使用。

        不过还是回到上一节提到CAPM模型存在的问题,相信正在读笔者文章的您不是一般的散户,我们自然不愿满足于CAPM的完美世界,因此笔者接下来讨论一些进阶玩法。

5. 折现率有多少进阶玩法

        事实上前人已经做了大量研究讨论如何消除假设条件并提高CAPM模型的有效性,其原理是在CAPM变量的基础上加入更多的变量及使用不同估计方式用来解释预期或真实回报率。

        笔者想到比较进阶的玩法有以下这些:

模型 门槛
CAPM 低阶
隐含回报率法
风险溢价叠加法
Fama-French三因子 进阶
Pastor Stambaugh Model
Carhart四因子
Fama-French五因子
BIRR宏观五因子
套利定价模型 中级
宏观因子模型
基本面因子模型
统计因子模型
混合因子模型
神经网络模型 高级

       表一:折现率估计方式一览(仅针对上市企业权益估值)

        一般来说学到进阶模型的前3个就够用了,因为前人已经做了大量实证检验研究有效性。而后面中高级这些模型只是个框架或者说方法论,不仅有一定专业性门槛,还需要很多专业判断和大量的实证检验。

        这些模型中最有名,贡献也最大的是Fama老爷爷的三因子模型。除了此之外的模型有的是计算预期回报率,有的是计算真实回报率;有的只考虑基本面因素,有的考虑宏观经济变量;有的考虑行为金融学因子,有的只考虑统计意义;还有的只是个理念框架,需要配合分析师的职业判断加入因子。最后的神经网络属于较新的方向,由于人工智能在金融领域兴起时间还不长,尚缺乏大量的实证检验,其认可度于接受度还很低,但门槛却是所有模型中最高的,笔者看过一些相关文献,总感觉都差点意思。因为动用复杂模型会出现一系列问题,很多时候不是模型越复杂越好,简单模型反而可以屏蔽很多噪音。总之现在嘛看菜吃饭,既然已经有经过实证检验的有效模型,我们就先讨论比较低阶点的FFM和PSM。

5.1 Fama-French三因子

        别看笔者只把FFM归类到进阶玩法,Fama老爷爷在资产定价这块作出了巨大贡献,1到100,如果说CAPM模型的诞生是0到1的突破,那么笔者认为Fama的三因子模型可称得上是1到80的进步(从贡献的意义上讲,不是统计意义)。到目前为止的后来者无论怎么提升,也只有剩下20分的空间了。其最主要的原因在于Fama做了大量的实证检验研究,他所筛选出的因子解释力度较强,以至于后来者无论怎么往上加因子也只能使得模型解释力度小幅提升而已,这一点在笔者后文模型中可以很明显看见。

        网上已经有很多文章解释三因子模型,其实PSM是在三因子模型基础上拓展得到的,实现一个PSM相当于还附带实现了一个CAPM+FFM,买一送二。笔者本来也不打算写FFM做无用功了,不过还是鉴于PSM是在三因子模型基础上拓展得到的,这里还是要简单介绍一下三因子模型,一步一步来。

        由于实证检验认为长期来看,小盘股回报优于大盘股,价值股优于成长股,FFM在CAPM的市场风险的基础上加入市值因子与价值因子:

1)市场风险因子(写做RMRF,Market return minus Risk free rate):与CAPM一样,市场收益率减去无风险利率,一般来说以新发行的国债收益率作为无风险利率并进行期限匹配。但是各个地方的习惯都不尽相同,例如美国分析师喜欢用10年期的国债收益率,因为把股票看做长期投资,考虑期限匹配原则;我们国家还有澳大利亚经常用1年的;学术界还有以三个月的短期的作为无风险利率,因为把时间拉长了终归还是有一定不确定性。总之各个地方都不太一样,笔者用之前主要考虑国债收益率波动情况,还需要预判下未来经济状态,综合考虑,不过文章里举例嘛毕竟随意,具体需要读者自行判断;

2)市值因子(写做SMB, Small minus Big):小盘股平均回报减大盘股平均回报,也可以采用做空大盘股,做多小盘股的投资组合回报率来衡量该因子;

3)价值因子(写做HML, High minus Low):价值股平均回报减成长股平均回报,也可以同上采用一多空策略的投资组合收益率衡量。判断价值股成长股的主要方式是看Book value/Price指标(PB指标的倒数),低PB(高BP)代表价值股,高PB(低BP)代表成长股;

        FFM之所以是High-Low是因为用的是PB指标的倒数,PB指标使用倒数有很多优点,关于这个原因这里就不展开了,后面如果出PB指标的内容在详细介绍。

综上,FFM可用公式二表示:

E(r_{i})=R_{f}+\beta _{i}^{market}RMRF+\beta _{i}^{size}SMB+\beta _{i}^{value}HML\, \, \, \, [2]

其中:

\beta^{market}是市场因子的敏感度,与CAPM一样,基准值为1,大于1表示对市场变化敏感,小于1则不敏感;

\beta^{size}是市值因子敏感度,基准值0,大于0偏向小盘股,小于0偏向大盘股

\beta^{value}是价值因子敏感度,基准值0,大于0偏向价值股,小于0偏向成长股

        网上还有很多文章写FFM的,笔者就不往下继续写了,还有问题的可以自行搜索或私信笔者。下面介绍笔者认为更有效一些的Pastor Stambaugh Model。

5.2 PSM

        PSM可以看做是Fama模型的拓展,进一步加入流动性因素。”流动性可以理解为不同类型的资产可以转换为现金的难易程度。在相当长的一段时间里,这一范畴在现代金融理论的框架内没有得到适当的考虑。因此,PSM理论框架补足了许多基本模型不考虑流动性问题”(Agata,2017)。

        PSM其实就是在FFM三因子基础上拓展了一项流动性,公式写做:

E(r_{i})=R_{f}+\beta _{i}^{market}RMRF+\beta _{i}^{size}SMB+\beta _{i}^{value}HML+\beta _{i}^{liq}LIQ\, \, \, \, [3]

        其中,Liq指的是流动性(liquidity),低流动性的公司要求更多流动性风险补偿,因此低流动性公司回报率>高流动性公司,一样也可以使用多空投资组合收益率衡量。基准值为0,大于0偏向低流动性,小于0偏向高流动性。

        鉴于别人都实证检验过了,笔者这里就不一个一个因子去讨论有效性了。这里顺带提一个Carhart四因子。其实和PSM一样,Carhart也是在FFM三因子基础上加入新的变量解释r, 但Carhart最后一个因子融入了行为金融学的动量效应概念,笔者以前曾经做过一点研究,难点在于动量效应很难去用一个统一的标准计量,放在A股市场上依旧要做大量的参数分析,一些研究使用的参数和估计方式虽然证明有效,但放在不同的时期结果有点不稳定,笔者认为问题还是对与动量效应的衡量上,如果您是研究Carhart的高手,欢迎交流。

        笔者目前认为,相比于行为金融学有点琢磨不定的动量效应,PSM的流动性其实更容易把握一些,既然脚下的路前人都为我们铺好了,直接大步向前走!

6.代码实现

        理论方面没问题,接下来就是数据和技术问题了。本文主要行情数据通过Tushare金融大数据平台API获取(Tushare数据),花两分钟注册即可以使用自己的API请求很多经常使用的行情数据,拯救笔者于写大量爬虫的水火之地。如果需要一些高频和特色数据则需要充一两百块达到一定的积分门槛,但是比起Wind, Choice动辄几千上万的接口费用,Tushare非常亲民了。不算是打广告,笔者推荐。

        先导入需要模块

import pandas as pd
import tushare as ts
from sklearn.linear_model import LinearRegression
import matplotlib.pyplot as plt
import seaborn as sns

        数据接口用法为:

pro = ts.pro_api(token) # 自己的密钥
cp = pro.daily(ts_code=cp, start_date=start, end_date=end) # 根据API技术文档输入请求参数

6.1 思路

        从多因子模型的公式不难看出还是和CAPM一样的回归分析,但难点在于市场数据每天都在变,不仅是时间序列数据,而且还有横截面数据。因此,对市场每一天的数据都要在全指数几千家公司数据中筛选一次哪些是大盘股,哪些是成长股,哪些流动性比较好,然后把它们筛选出来以市值加权求出当天的投资组合回报率。

        刨去CAPM的因子,剩下三个因子每个都会形成两个投资组合,要处理六个投资组合五到十年的数据,最后和目标公司数据进行回归。如果还是在没有本地数据库的基础上请求API,这工作量和时间要求其实还是挺大的。几千家公司这么多年的数据用Excel去筛选计算那怕是要直接去世,更不要说后面那几个难度更大的模型。因此笔者说后面这些模型都有较高的门槛,光这么多数据,没有API或者数据库支持就得刷掉一堆人,深入理解背后的专业原理再刷掉一堆人,敲代码实现又刷掉一堆人。

        不过嘛,它其实说难也不难,筛选,然后处理数据罢了。并且好消息是只要模型跑了一遍拿到风险溢价数据,后面其实就不用再跑了,因为对于同一个市场,风险因子的溢价是一样的,不同的只是需要回归的风险敏感系数

6.2 数据准备

         通过上面的分析,首先要获取上证指数5年的全景数据,看网上有写矩阵运算的大佬,笔者试了一下,发现由于上市时间,退市时间,停牌时间都不一样,数据参差不齐,如果用矩阵运算很容易对不上出错,笔者这里采用给每个公司创一个对象的方法,后面直接调用属性去一个个判断,坏处在于肯定会比矩阵运算慢, 但是谁让python面向对象呢,既然是优势就要发扬光大,一时对象一时爽,一直对象一直爽,谁用谁知道

        先获取上交所所有上市,退市和停牌的股票列表:

data = pro.stock_basic(exchange='SSE', list_status='L', fields='ts_code')

stock_list = []
for i in ["D","P","L"]:
    data = pro.stock_basic(exchange='SSE', list_status=i, fields='ts_code')
    stock_list.extend(data["ts_code"].values)
print(stock_list, len(stock_list))

        截至笔者发文,共2220家公司在上交所上市。

        接下来用这个接口(需要两千积分)遍历所有公司2017年1月到今年9月的数据,共五年多一点点,虽然这个接口在技术文档里没有这个单个公司时间序列数据请求的方法,但笔者试了一下还是可以的:

companies_list = []
variables = "ts_code,trade_date,close,turnover_rate,volume_ratio,pb,circ_mv"
for i in stock_list:
    df = pro.query('daily_basic', ts_code=i, start_date='20170101', end_date="20220924", fields=variables)[::-1]

        下面加入类对象,表格数据清理,最后整合一下上面的代码:

class company:
    def __init__(self, date, code, mv, pb, turn_over, r):
        self.date = date
        self.code = code
        self.mv = mv
        self.pb = pb
        self.turn_over = turn_over
        self.r = r


stock_list = []
for i in ["D","P","L"]:
    data = pro.stock_basic(exchange='SSE', list_status=i, fields='ts_code')
    stock_list.extend(data["ts_code"].values)

variables = "ts_code,trade_date,close,turnover_rate,volume_ratio,pb,circ_mv"
companies_data = []
for i in stock_list:
    df = pro.query('daily_basic', ts_code=i, start_date='20170101', end_date="20220924", fields=variables)
    if len(df) > 0:
          df = df[::-1]
          df.dropna(inplace=True)
          df.drop_duplicates(inplace=True)
          # 要用收盘价算涨幅,最后一天舍弃
          date = np.array(df["trade_date"].values[1:]) # 交易日期
          code = np.array(df["ts_code"].values[1:]) # 公司代码
          mv = np.array(df["circ_mv"].values[1:]) # 流通市值
          pb = np.array(df["pb"].values[1:])
          turn_over = np.array(df["turnover_rate"].values[1:])
          close_start = np.array(df["close"][:len(df)-1])
          close_next = np.array(df["close"][1:])
          r = (close_next - close_start) / close_start # 涨幅
          companies_data.append(company(date, code, mv, pb, turn_over, r))

        上面的代码还嫌慢的话还可以同时开几个线程获取数据,关于多线程可以参考笔者金融数据获取的往期文章,这里也不做展开了,如果不会多线程直接跑上面代码也一样,只是慢而已

        改写上面的遍历循环成函数形式,加入互斥锁:

def data_request(codes, companies_data):
    variables = "ts_code,trade_date,close,turnover_rate,volume_ratio,pb,circ_mv"
    lock = threading.Lock()
    for i in codes:
        lock.acquire()
        df = pro.query('daily_basic', ts_code=i, start_date='20170101', end_date='20220924', fields=variables)
        if len(df) > 0:
            df = df[::-1]
            df.dropna(inplace=True)
            df.drop_duplicates(inplace=True)
            date = np.array(df["trade_date"].values[1:]) # 要算涨幅,最后一天舍弃
            code = np.array(df["ts_code"].values[1:]) # 公司代码
            mv = np.array(df["circ_mv"].values[1:]) # 流通市值
            pb = np.array(df["pb"].values[1:])
            turn_over = np.array(df["turnover_rate"].values[1:])
            close_start = np.array(df["close"][:len(df)-1])
            close_next = np.array(df["close"][1:])
            r = (close_next - close_start) / close_start # 涨幅
            companies_data.append(company(date, code, mv, pb, turn_over, r))
        else:
            pass
        lock.release()

               使用threading开启多线程,下面代码开了五个,具体开多少看自己电脑性能吧:

stock_list = []
for i in ["D", "P", "L"]:
    data = pro.stock_basic(exchange='SSE', list_status=i, fields='ts_code')
    stock_list.extend(data["ts_code"].values)
quin = len(stock_list[:15]) // 5
companies_data, threads_pool = [], []
start_time = datetime.datetime.now()
for i in range(0, 6):
    t1 = threading.Thread(target=data_request, args=(stock_list[quin * i:quin * (i + 1)], companies_data),
                          name="task{}".format(i))
    threads_pool.append(t1)
    t1.start()
for i in threads_pool:
    i.join()
print("总列表", len(companies_data))
end_time = datetime.datetime.now()
print("五线程执行程序时间", end_time - start_time)

        不过积分不到5千开多线程是有请求限制的,开超过三个线程请求很快会超过200次,但三线程2000积分也可以正常运行不会报错:

Exception: 抱歉,您每分钟最多访问该接口200次,权限的具体详情访问:https://tushare.pro/document/1?doc_id=108。

        都执行后companies_data里就存储了上证所有公司近五年多以来的所有计算所需参数。

        下面获取CAPM模型所需参数,接着使用上面存储的类对象创建3个因子的相关投资组合。

6.3 RMRF因子

        CAMP模型的因子,笔者目标公司为上证指数成分股,直接请求上证指数近5年日线数据,其实用月线数据也可以,有人认为月线数据可以屏蔽很多噪音,但笔者认为相对也会丢失很多信号,笔者下面的参数及变量选择均是举例之用,仅供参考:

mk = pro.index_daily(ts_code=’000001.SH‘, start_date=’20170101‘, end_date=’20220924‘)
print(mk)

        无风险利率,这里笔者用1年期最新国债收益率,国债数据Tushare没有,写个迷你爬虫:

url = "https://yield.chinabond.com.cn/cbweb-cbrc-web/cbrc/historyQuery?startDate" \
      "=2022-09-23&endDate=2022-09-24&gjqx=0&qxId=ycqx&locale=cn_ZH&mark=1"
content = pd.read_html(url)
table = content[1]
rf = float(table.loc[1,4])/100
print('Risk_free rate(1 year):', table.loc[1, 4], "%")

        RMRF需要的数据齐活。

6.4 SMB因子, HML因子及LIQ因子

        从这三个因子就需要用到刚才的类对象列表数据了。因为数据都在一块笔者就合在一起写三个因子了。先选择上证正常交易的日期作为基准, 在刚刚的类对象中遍历当天正常交易的个股。

index_trade_date = pro.index_daily(ts_code='000001.SH', start_date='20170101', end_date='20220924')["trade_date"].values

        后面只要在这个交易日列表里按时间维度推进,首先将每天交易的所有股票三因子数据分别拉进三个大列表,然后分别对高低阈值进行分位数判断。例如上证所有个股在2017年1月5号的当天收盘市值拉一个大列表,其中超过80%分位数的判定为大市值公司,小于20%分位数的判定为小市值公司。其它几个因子如法炮制。

        判断出阈值后只需要再遍历当天数据然后把符合条件的公司全拉出来,该存列表存列表,最后对回报率进行市值加权,非常简单。

date_times = []
big_ret = []
small_ret = []
high_ret = []
low_ret = []
liq_ret = []
illiq_ret = []
for i in index_trade_date:
    big, small, high, low, liq, illiq = [], [], [], [], [], []
    mv_lst, pb_lst, turnover_lst, trading_companies = [], [], [], []
    for company in companies_data:
        if i in company.date:  # 拉取当天所有交易股票的三个因子数值
            index = list(company.date).index(i)  # 定位到当天的索引
            mv_lst.append(company.mv[index])
            pb_lst.append(company.pb[index])
            turnover_lst.append(company.turn_over[index])
            trading_companies.append(company)
        else:
            pass
    if len(mv_lst) > 0:
        date_times.append(i)  # 拿这个给最后生成的表格一个时间索引
        mv_big = np.percentile(mv_lst, 70)  # 超过70分位阈值则认为是大市值公司
        mv_small = np.percentile(mv_lst, 30)  # 低于30分位阈值则认为是小市值公司
        pb_high = np.percentile(pb_lst, 70)
        pb_low = np.percentile(pb_lst, 30)
        liq_good = np.percentile(turnover_lst, 70)
        liq_bad = np.percentile(turnover_lst, 30)

        # 得到当天的三因子阈值后判断每个公司是否符合阈值条件
        total_mv_big, total_mv_small = 0, 0  # 几个投资组合总市值
        total_mv_high, total_mv_low = 0, 0
        total_mv_ilq, total_mv_illiq = 0, 0
        for company in trading_companies:
            index = list(company.date).index(i)  # 定位当天索引
            if company.mv[index] >= mv_big:
                big.append(company.r[index] * company.mv[index])  # 乘市值方便后面进行市值加权
                total_mv_big += company.mv[index]
            else:
                if company.mv[index] <= mv_small:
                    small.append(company.r[index] * company.mv[index])
                    total_mv_small += company.mv[index]
                else:
                    pass

            if company.pb[index] >= pb_high:
                high.append(company.r[index] * company.mv[index])
                total_mv_high += company.mv[index]
            else:
                if company.pb[index] <= pb_low:
                    low.append(company.r[index] * company.mv[index])
                    total_mv_low += company.mv[index]
                else:
                    pass

            if company.turn_over[index] >= liq_good:
                liq.append(company.r[index] * company.mv[index])
                total_mv_ilq += company.mv[index]
            else:
                if company.turn_over[index] <= liq_bad:
                    illiq.append(company.r[index] * company.mv[index])
                    total_mv_illiq += company.mv[index]

        # 以市值加权求出每个因子当天的平均回报
        big_ret.append(np.sum(big) / total_mv_big)
        small_ret.append(np.sum(small) / total_mv_small)
        high_ret.append(np.sum(high) / total_mv_high)
        low_ret.append(np.sum(low) / total_mv_low)
        liq_ret.append(np.sum(liq) / total_mv_ilq)
        illiq_ret.append(np.sum(illiq) / total_mv_ilq)

        上面的操作也是可以用多线程对几个因子计算进行加速的,主要是前面数据请求如果不用线程加速会慢上很多,笔者这里就不进行加速了,仅作举例。

        把最后计算出来的因子导入字典存个表格导入csv吧,这样下次就不用花很长时间请求数据算这些因子了:

data_dic = {
            "date": date_times,
            "big_r": big_ret,
            "small_r": small_ret,
            "high_pb_r": high_ret,
            "low_pb_r": low_ret,
            "liq_r": liq_ret,
            "illiq_r": illiq_ret
           }
data = pd.DataFrame(data_dic)
print(data)
data.to_csv("risk_factor.csv")

         运行所得因子数据如下,单线程从头开始运行大概需要2小时左右:

          date     big_r   small_r  high_pb_r  low_pb_r     liq_r   illiq_r
0     20220923 -0.003312 -0.024727  -0.009208  0.001549 -0.022543  0.001844
1     20220922 -0.002248 -0.003328  -0.005086 -0.001887  0.010462 -0.017936
2     20220921 -0.001845  0.001306  -0.013389  0.006929  0.005972 -0.007036
3     20220920  0.000286  0.016349   0.008399 -0.005073  0.020536 -0.018587
4     20220919 -0.000803 -0.017215  -0.000701 -0.003029 -0.003053  0.001629
...        ...       ...       ...        ...       ...       ...       ...
1388  20170110 -0.002582 -0.001243  -0.001454 -0.002697 -0.000341 -0.021626
1389  20170109  0.005502  0.004561   0.005048  0.005237  0.020685  0.028170
1390  20170106 -0.000935 -0.012680  -0.006127 -0.000256 -0.005270 -0.007484
1391  20170105  0.003315  0.000573  -0.001991  0.004379  0.009601  0.017124
1392  20170104  0.006871  0.008199   0.017371  0.005390  0.020601  0.051140

[1393 rows x 7 columns]
单线程数据处理执行程序时间 1:50:46.584115

Process finished with exit code 0

         运行完成后就可以从本地读取了,看看各个因子分布:

df = pd.read_csv("C:/Users/Administrator/Desktop/risk_factor.csv")
smb = df["small_r"] - df["big_r"]
hml = df["low_pb_r"] - df["high_pb_r"]
liq = df["illiq_r"] - df["liq_r"]

sns.distplot(smb, color="blue", label="SMB")
sns.distplot(hml, color="yellow", label="HML")
sns.distplot(liq, color="red", label="LIQ")
plt.legend()
plt.show()

        流动性溢价分布肥尾矮峰,市值和价值两个分布相对于流动性因子都很集中:

浅谈估值模型 (三): 回报率r的进阶玩法——Fama-French及PSM(Pastor Stambaugh Model)_第2张图片

 图二:因子溢价分布

6.5 敏感系数

        先把几个因子都取出来算好, 然后拿目标公司日线数据, 无风险回报在最开始已经爬到了。这里因为停牌交易时间不一样,需要在两个表中选出交集部分,本来对表格内用df[~df["date"].isin(trade_date)]可以选出不一样的行的,但不知道为什么程序运行就是匹配不上,笔者只好用了三个循环把不一样的交易日数据剔除,有表格操作的大神可以指点指点:

code = "code" # 目标公司tushare代码

df = pd.read_csv("C:/Users/Administrator/Desktop/risk_factor.csv")
stock = pro.daily(ts_code=code, start_date='20170101', end_date='20220924') # 目标公司数据
mkt = pro.index_daily(ts_code='000001.SH', start_date='20170101', end_date='20220924') # 大盘

for i in stock["trade_date"].values:
    if int(i) not in df["date"].values:
        stock.drop(list(stock["trade_date"].values).index(i), inplace=True)
        stock.index = range(len(stock))

for i in df["date"].values:
    if str(i) not in stock["trade_date"].values:
        df.drop(list(df["date"].values).index(i), inplace=True)
        df.index = range(len(df))
        
for i in mkt["trade_date"].values:
    if int(i) not in df["date"].values:
        mkt.drop(list(mkt["trade_date"].values).index(i), inplace=True)
        mkt.index = range(len(mkt))
        
rf_daily = (1+ rf/100)**(1/365) - 1 # %数据全都要转化成小数
mkt = mkt["pct_chg"] / 100 - rf_daily # 市场因子
smb = (df["small_r"] - df["big_r"])/100 # 市值因子
hml = (df["low_pb_r"] - df["high_pb_r"])/100 # 价值因子
liq = (df["illiq_r"] - df["liq_r"])/100 # 流动性因子
stock_r = stock["pct_chg"] / 100 - rf_daily # 目标公司回报

6.6 回归分析

        最后就是回归了,网上很多用sm的,笔者喜欢用smf只是因为它的操作和R语言操作很类似:

import statsmodels.formula.api as smf
# RM 市场因子, # RP个股风险溢价
data = pd.DataFrame({"RM": mkt.values, "SMB":smb.values, "HML": hml.values, "LIQ": liq.values, "RP":stock_r.values})
reg = smf.gls(formula='RP~-1+RM+SMB+HML+LIQ',data=data)
mod = reg.fit()
mod.summary()

        得到部分关键结果如下:

GLS Regression Results
Dep. Variable:	RP	R-squared:	0.333
Model:	GLS	Adj. R-squared:	0.331
Method:	Least Squares	F-statistic:	173.0

	        coef	std err	  t	    P>|t|	[0.025	0.975]
Intercept	0.0006	0.001	0.822	0.411	-0.001	0.002
RM	        1.3985	0.115	12.129	0.000	1.172	1.625
SMB	        59.9619	6.458	9.285	0.000	47.294	72.630
HML	       -30.5742	6.132	-4.986	0.000	-42.603	-18.545
LIQ	        -5.2618	2.160	-2.436	0.015	-9.499	-1.025

        可以看到截距项是不显著的,也可以理解嘛,公式里本身也没有截距项,下面回归写“RP~-1+”,那个-1是指数据中心化不要截距项了

reg = smf.gls(formula='RP~-1+RM+SMB+HML+LIQ',data=data)
mod = reg.fit()
mod.summary()

        运行全部结果展示如下:

GLS Regression Results
Dep. Variable:	RP	R-squared (uncentered):	0.333
Model:	GLS	Adj. R-squared (uncentered):	0.331
Method:	Least Squares	F-statistic:	173.3
Date:	Sun, 25 Sep 2022	Prob (F-statistic):	2.15e-120
Time:	16:53:06	Log-Likelihood:	3188.4
No. Observations:	1390	AIC:	-6369.
Df Residuals:	1386	BIC:	-6348.
Df Model:	4		
Covariance Type:	nonrobust		
        coef	std err	   t	P>|t|	[0.025	0.975]
RM	    1.4202	0.112	12.655	0.000	1.200	1.640
SMB	    58.7254	6.279	9.352	0.000	46.408	71.043
HML	   -30.7000	6.129	-5.009	0.000	-42.724	-18.676
LIQ	    -5.7750	2.067	-2.793	0.005	-9.831	-1.719
Omnibus:	195.043	Durbin-Watson:	2.039
Prob(Omnibus):	0.000	Jarque-Bera (JB):	605.842
Skew:	0.702	Prob(JB):	2.77e-132
Kurtosis:	5.914	Cond. No.	104.

         当然啦,实现一个PSM = CAPM + FFM 三合一大礼包,来看看CAPM表现吧:

import statsmodels.formula.api as smf
reg = smf.gls(formula='RP~-1+RM',data=data)
mod = reg.fit()

## 主要结果
GLS Regression Results
Dep. Variable:	RP	R-squared (uncentered):	0.234
Model:	GLS	Adj. R-squared (uncentered):	0.233
Method:	Least Squares	F-statistic:	423.4
		
	coef	std err	  t	    P>|t|	[0.025	0.975]
RM	1.3436	0.065	20.576	0.000	1.216	1.472

         可以看到,PSM在CAPM基础上把R方从0.23提高到0.33,提高了约43%的表现 (不过从相关系数可以看出这其中贡献最大的是FFM加的两个因子)。

7. 因子有效性讨论

        根据回归结果,笔者目标公司的预期回报可通过如下公式计算:

E(r_{i})=R_{f}+\beta _{i}^{market}RMRF+\beta _{i}^{size}SMB+\beta _{i}^{value}HML+\beta _{i}^{liq}LIQ

        当然这几个因子是需要算一下全年平均值和年化回报匹配起来,这样回报体现的是过去一年的平均状态。不过用单日的算然后年化也可以,这样算出来的回报就体现的是市场最新状态。接下来就可以根据系数计算出预期回报了,注意tushare请求的涨跌幅数据全是百分制的,处理的时候要全部转化成小数,如果在一开始请求的时候全部改成小数就不会再后面一直拿100除了:

mkt_r = np.mean(pro.index_daily(ts_code='000001.SH', start_date='20170101', end_date='20220931')["pct_chg"])/100 # 大盘历史平均
Er = rf_daily + 1.4202 * (mkt_r - rf_daily) -58.7254 * np.mean(data["SMB"][0:250])/100 -30.7000 * np.mean(data["HML"][0:250])/100 -5.775 * np.mean(data["LIQ"][0:250])/100
print((1 + Er)**365 -1) # 几个因子笔者用了最近一年的均值

# 0.025705644748385437

        算得预期回报2.57%的样子,CAPM再算一下:

CAPM = rf_daily + 1.3436 * (mkt_r - rf_daily)
print((1 + CAPM)**365-1)

# 0.020763357720487763

       CAPM所得回报率2.08%左右,低于PSM,原因在于几个风险因子贡献的溢价。正是由于这些风险因子溢价,理论上PSM所求出的回报率往往都是高于CAPM的。值得注意的是PSM算出的也依然是仅考虑系统性风险的回报率,鉴于该公司大数据业务所带来的未来景气度和高增速,笔者认为该回报率是偏低的,毕竟是依旧历史数据,还是需要做出其它调整,这属于模型外的东西,笔者就不展开讨论了。

        通过回归分析,笔者根据换手率所选取的流动性因子在统计意义上是有效的,只是它对回报率的贡献不大,正如笔者在前文所说的,后面的人在后面加因子但是解释力度不一定有FFM三因子强。

8. 笔者的话

        多因子模型是单因子的拓展,它虽然提高了解释力,但其中依旧有很多问题,例如多元回归的多重共线性及单因子模型回报率的非负性

        多重共线性不用多说了,多因子线性模型的一生之敌。但这些已经经过检验的模型其实还好,主要是实务中自己加因子时需要注意,因子太多也不一定是好事。

        其次是单因子模型回报率的非负性,相信用CAPM算的回报率没几个会算出负数吧,这和我们潜意识的感知是符合的——投资就是要拿回报的,因此在折现的时候没几个人会拿负的折现率算回报吧。但多因子模型算出来的回报很有可能就是个负值,从之前图二的因子分布大家就不难看出,市场流动性风险因子散得很开,也就是说在极端情况下很容易出现敏感系数不高甚至为负,但该因子的风险溢价被市场打得很高,以至于其它几个正的因子项全部被冲掉最后算出个负回报。这出乎意料的合乎现实——投资极有可能是亏的,而且所谓七亏二平一赚,大多数人都是亏的。一个是潜意识愿意相信的信念,一个是不以主观意志为转移的现实,您站哪边?

        最后笔者再谈一点是之前看到的一篇论文,谈了FFM三因子在中国市场的本土化(Liu et al., 2019),其中提到了壳资源污染市值因子,还有价值因子的问题。该文章认为中国市场的有效性低于美国等成熟市场,因此FFM的模型很大程度上很容易失效,并在FFM基础上做了改动,例如在因子计算中舍弃后30%的垃圾公司。和笔者一样,他们也在三因子基础上拓展出以流动性为计量标准的第四因子(笔者是参考PSM,在没看见他们论文之前就加进去了)。该论文认为在刨除30%的壳污染公司后,模型更适用于中国市场。笔者认为注册制改革,监管高压之下,这种壳资源以后还是越来越不值钱,市场有效性也会随之提升,给的溢价自然就会降低,因此在本文模型中,笔者没有把后30%的公司舍弃。论文链接附下面参考文献了,在国内的读者可能需要VPN,如果打不开链接又想要这篇论文的可以评论或者私信笔者。

        回报率和折现率是个很大的话题,里面还有很多问题值得讨论,尽管笔者2天洋洋洒洒2万字,名字依旧是浅谈。最后,创作不易,点赞关注评论三连。

        您若不弃,我们风雨共济。

浅谈估值模型 (三): 回报率r的进阶玩法——Fama-French及PSM(Pastor Stambaugh Model)_第3张图片

参考文献:

Agata, G.S. (2017).'The Multifactorial PSM: Explaining The Impact Of Liquidity On The Rate Of Return Based On The Example Of The Warsaw Stock Exchange', Equilibrium. Quarterly Journal of Economics and Economic Policy, Institute of Economic Research, vol. 12(2), p. 211-228,https://ideas.repec.org/a/pes/ierequ/v12y2017i2p211-228.html

Liu, J., Stambaugh, R.F., & Yuan, Y. (2019). Size and value in China. Journal of Financial Economics, 134(1), 48–69. https://doi.org/10.1016/j.jfineco.2019.03.008 

你可能感兴趣的:(浅谈估值模型,python,金融)