Fama-French 三因子模型在A股市场的实证研究

https://uqer.io/community/share/5784b3d1228e5b8a09932d9e

Fama-French 三因子在A股市场的实证研究

Fama-French三因子模型无疑是量化领域最经典的模型之一,该模型的提出是在论文《commom risk factors in returns on bonds and stocks>里,本帖本着学习的精神对其进行了学习,并使用论文中的方法在中国A股市场上进行了实证。\begin{align}E(R(t))=R_f(t)+bE(R_M(t)-R_f(t))+sE(SMB(t))+hE(HML(t))\end{align}

RMRf表示的是市场因子,SMB表示的是规模(市值)因子,HML表示账面市值比因子

一.背景

资本资产定价模型(CAPM)问世以后,许多学者对其进行了实证研究,如Black和Scholes(1972)及Fama(1973)的检验证明,对1969年以前的数据而言,资本资产定价模型是有效的,而对之后的数据,却缺乏说服力。在横截面数据里,股票的平均收益和市场β相关性很低,因而更多影响股票收益的因素亟待发掘。

Fama和French(1992)研究了市场β, 市值(size), 账面市值比(book-to-market equity),财务杠杆(leverage) 和市盈率(E/P)对平均收益的影响。横截面回归后发现,在独立检验四者对平均收益的影响时,四者都表现出了很强的解释能力,而β很弱;在进行多变量回归时,市值和账面市值比这两个因子吸收了另两个因子的解释能力,成为了解释平均收益的决定性变量

1993年,Fama和French的论文《commom risk factors in returns on bonds and stocks〉正式标志着三因子模型的建立。在该论文里,他们不仅研究了影响股票收益的因子模型,还研究了对债券收益的因子模型;更重要的是,不同于以往的横截面回归,该论文使用了Black,Jensen和Scholes的时间序列回归方法,对影响股票收益的市场超额收益,规模和账面市值比三个因子进行了实证研究。

下面,本帖就论文里对股票三因子模型的研究思路,在优矿平台上对中证800成分股从2007.6到2016.5的数据进行了实证研究,以验证三因子模型的有效性。

二.研究思路

1.Black-Jensen-Scholes时间序列回归

横截面回归大家都非常熟悉,无论是单变量还是多变量回归,都是在研究解释变量对响应变量的解释能力。Black-Jensen-Scholes时间序列回归的方法是Black,Jensen和Scholes所提出来验证CAPM的。早期的验证方法是先使用一个单只股票的时间序列回归估计贝塔,再用横截面回归验证CAPM推出的假设。但是这样回归会有误差项存在相关性,贝塔非平稳等问题,时间序列回归则避免了这些问题。即根据前一期估计的贝塔值对股票排序再进行分组,分别估计各投资组合的阿尔法和贝塔,每五年重新估计贝塔,然后检验各个投资组合的阿尔法是否显著为0,从而验证CAPM\begin{align}E(R(t))=R_f(t) +\beta(E(R_M(t))-R_f(t))\end{align}

2.解释变量

解释变量就是我们需要验证的三个因子,市场超额收益,规模和账面市值比。我们要按照论文里的思路对其进行处理。

1)分组

把股票按每年5月末时的市值(size)大小进行排序,按照50%分位值把股票分为S(small)和B(big)两组;

再依据5月末时的账面市值比(我们取1/PB)大小对800只股票进行排序,分为L(low,30%),M(medium,40%),H(high,30%)三组;

再分别对S,B和L,M,H取交集,股票即被分为了SL,SM,SH,BL,BM,BH六组。

也就是说,分组每年5月末进行一次,800只股票每次被重新分为了SL,SM,SH,BL,BM,BH六组,前一年6月到第二年5月重新分组时的投资组合都是一样的

这里为什么要按市值分为两组,按账面市值比分为三组呢?是因为账面市值比有更强的作用,我们要把它分得更细。

(PS:论文里是6月末按照市值大小分组,账面市值比依据的是前一年末时的数据,个人以为5月末也没事)

查看全部

下面我们要计算每个投资组合的月收益率,计算投资组合的月收益率时,要算市值加权的收益率,这是为了最小化方差(风险)

1
#得到投资组合x从Year的6月到Year+1的5月的月收益率序列
2
def get_returnMonthly(x,Year):
3
    #先用交易日日历得到Year的5月到Year+1的5月的月末交易日日期
4
    from CAL.PyCAL import *
5
    data=DataAPI.TradeCalGet(exchangeCD=u"XSHG",beginDate=str(Year*10000+501),endDate=str((Year+1)*10000+601),field=['calendarDate','isMonthEnd'])
6
    data = data[data['isMonthEnd'] == 1]
7
    date= map(lambda x: x[0:4]+x[5:7]+x[8:10], data['calendarDate'].values.tolist())
8
    #调用投资组合x每只股票每个月末的市值,收盘价用来计算收盘价
9
    returnMonthly=np.zeros(12)
10
    for i in range(12):
11
        inf1=DataAPI.MktEqudAdjGet(tradeDate=date[i],ticker=x,field=u"ticker,closePrice").set_index('ticker')  #前一个月的收盘价
12
        inf2=DataAPI.MktEqudAdjGet(tradeDate=date[i+1],ticker=x,field=u"ticker,marketValue,closePrice").set_index('ticker')   #当月的收盘价和市值
13
        Return=pd.concat([inf2,inf1],axis=1)
14
        Return.columns=['Weight','Return','WReturn']                   #计算每只股票收益率和市值加权的权重以及两者的乘积
15
        Return['Weight']=Return['Weight']/Return['Weight'].sum()
16
        Return['Return']=Return['Return']/Return['WReturn']-1
17
        Return['WReturn']=Return['Weight']*Return['Return']
18
        returnMonthly[i]=Return['WReturn'].sum()
19
    return returnMonthly
查看全部

2)因子

市值因子:\begin{align}SMB = 1/3(SL+SM+SH)-1/3(BL+BM+BH)\end{align}表示的是由于公司规模不同造成的风险溢价

账面市值比因子:\begin{align}HML = (SH+BH)/2-(SL+BL)/2\end{align}表示由于账面市值比不同造成的风险溢价

可以看出因子的值是一个市值加权月收益率序列,我们研究了九年的数据,所以因子的长度是9*12=108

查看全部
1
#加载画图需要用的包
2
import matplotlib as mpl
3
import matplotlib.pyplot as plt
4
mpl.style.use('ggplot')
5
import seaborn as sns
查看全部

下面我们先看一看我们得到的六个组合的市值加权月收益率的情况,直观上符合常理

查看全部
<matplotlib.axes._subplots.AxesSubplot at 0x7a79650>

市场因子:\begin{align}R_M-R_f\end{align}RM取的就是中证800指数的收益,Rf取的是银行间质押式回购利率_同业拆借中心R007

1
#先用交易日日历得到Year的5月到Year+1的5月的月末交易日日期
2
from CAL.PyCAL import *
3
data=DataAPI.TradeCalGet(exchangeCD=u"XSHG",beginDate='20070501',endDate='20160601',field=['calendarDate','isMonthEnd'])
4
data = data[data['isMonthEnd'] == 1]
5
date = map(lambda x: x[0:4]+x[5:7]+x[8:10], data['calendarDate'].values.tolist())
6
RmMonthly=np.zeros(108)
7
RfMonthly=np.zeros(108)
8
for i in range(108):
9
    index1=DataAPI.MktIdxdGet(tradeDate=date[i],indexID=u"000906.ZICN",field=u"closeIndex") #上月指数收盘
10
    index2=DataAPI.MktIdxdGet(tradeDate=date[i+1],indexID=u"000906.ZICN",field=u"closeIndex")  #当月指数收盘
11
    RmMonthly[i]=index2['closeIndex'][0]/index1['closeIndex'][0]-1
12
    rf=DataAPI.ChinaDataInterestRateInterbankRepoGet(indicID=u"M120000068",beginDate=date[i+1],endDate=date[i+1],field=u"dataValue")  #当月无风险收益
13
    RfMonthly[i]=rf['dataValue'][0]/100/12    #给出的是年化无风险收益,这里需要转化成月的
14
MF=RmMonthly-RfMonthly  #市场因子
查看全部

三个因子我们都得到了,再来看看三个因子的状况:

查看全部
 MFSMBHML
count108.000000108.000000108.000000
mean0.0012370.014489-0.001268
std0.0961890.0589290.049805
min-0.262845-0.209948-0.157205
25%-0.063134-0.014343-0.025673
50%0.0052960.014957-0.004887
75%0.0588380.0514840.022718
max0.1926180.2149160.248741

到这里,我们的三个因子就处理完了,三个解释变量都做成了数组,下面可以分析一下各个因子之间的相关系数

1
x=np.zeros((3,108))
2
x[0]=MF
3
x[1]=SMB
4
x[2]=HML
5
Correlations=pd.DataFrame(np.corrcoef(x))
6
Correlations.columns=['MF','SMB','HML']
7
Correlations.index=['MF','SMB','HML']
8
Correlations
查看全部
  MF SMB HML
MF 1.000000 0.212499 -0.031912
SMB 0.212499 1.000000 -0.424261
HML -0.031912 -0.424261 1.000000

3. 响应变量

首先我们将股票按之前的方法分为25个组合,即:

在每年5月末,按照市值大小将股票排序并分为5组,然后按照账面市值比大小把股票分为5组,交叉取交集,得到5*5=25个股票组合

也就是说,我们将作25个回归,每次回归时的解释变量都一样,响应变量不同

查看全部

计算25个股票组合,每个组合的市值加权月收益率序列

1
EReturn=np.zeros((25,12*9)) #用于存储25个组合的超额收益序列
2
for i in range(25):
3
    a=[]
4
    for Year in [2007,2008,2009,2010,2011,2012,2013,2014,2015]:
5
        Group25=get_25groups(str(Year*10000+531))   #每年进行分组
6
        a=a+(get_returnMonthly(Group25[i].dropna().tolist(),Year)).tolist()   #收益率转化为list,方便每年相加
7
    EReturn[i]=np.array(a)-RfMonthly
查看全部

我们看一下25个组合平均每年的公司数:

查看全部
 small_BE/ME123big_BE/ME
small_size12.88888927.88888935.77777841.33333341.888889
125.33333334.66666736.77777834.55555628.666667
239.55555634.22222234.44444425.55555625.555556
344.11111136.66666727.33333328.00000023.888889
big_size38.11111126.22222225.11111130.22222240.000000

还可以看一下25个组合平均每年的总市值大小,验证一下分组的正确性:

1
MarketValue=np.zeros((25,9)) 
2
for i in range(25):
3
    for j in range(9):
4
        breakpoint=str((j+2007)*10000+531)
5
        Group25=get_25groups(breakpoint)  #每年进行分组
6
        C=DataAPI.MktEqudGet(ticker='000028',beginDate=str(int(breakpoint)-20),endDate=breakpoint,field=u"ticker,tradedate")
7
        breakpoint=filter(lambda x:x.isdigit(),C.iat[len(C)-1,1])                             #取breakpoint前最近一个交易日日期
8
        data=DataAPI.MktEqudGet(tradeDate=breakpoint,ticker=Group25[i].dropna().tolist(),field=u"ticker,marketValue").dropna()
9
        MarketValue[i][j]=data['marketValue'].sum()
10
MarketValue_mean=np.zeros(25)
11
for i in range(25):
12
    MarketValue_mean[i]=MarketValue[i].mean()
13
MV=pd.DataFrame(MarketValue_mean.reshape(5,5))
14
MV.columns=['small_BE/ME','1','2','3','big_BE/ME']
15
MV.index=['small_size','1','2','3','big_size']
16
MV
查看全部
  small_BE/ME 1 2 3 big_BE/ME
small_size 6.007957e+10 1.175193e+11 1.455794e+11 1.798913e+11 1.646767e+11
1 1.745471e+11 2.368089e+11 2.355071e+11 2.167016e+11 1.717474e+11
2 3.920921e+11 3.309705e+11 3.278397e+11 2.391956e+11 2.118269e+11
3 7.007655e+11 5.851388e+11 4.300714e+11 4.208436e+11 4.202868e+11
big_size 2.054059e+12 2.041996e+12 2.416426e+12 3.877098e+12 7.510095e+12

上面的股票组合从左到右,账面市值比越来越大;从上往下,市值越来越大,说明我们的分组是正确的

看一下25个组合超额收益的均值和方差:

查看全部
 small_BE/ME123big_BE/ME
small_size0.0254330.0227990.0434280.0229990.019819
10.0192330.0224190.0233660.0223560.016953
20.0165950.0155790.0173920.0170740.020958
30.0194330.0121990.0149420.0136590.009995
big_size0.0061330.0008010.0060650.0075800.005248
1
EReturn_std=np.zeros(25)
2
for i in range(25):
3
    EReturn_std[i]=EReturn[i].std()
4
std=pd.DataFrame(EReturn_std.reshape(5,5))
5
std.columns=['small_BE/ME','1','2','3','big_BE/ME']
6
std.index=['small_size','1','2','3','big_size']
7
std
查看全部
  small_BE/ME 1 2 3 big_BE/ME
small_size 0.125990 0.111912 0.216930 0.111223 0.109084
1 0.110347 0.113726 0.110196 0.116036 0.111958
2 0.104115 0.105876 0.110242 0.112561 0.110588
3 0.106525 0.106846 0.107491 0.102590 0.110776
big_size 0.097351 0.097236 0.100561 0.102553 0.092128

三.回归和结果

1.回归一

CAPM回归模型:\begin{align}R(t)-R_f(t) = a+b(R_M(t)-R_f(t))+e(t)\end{align}这就是经典的CAPM模型,我们可以检验一下它在中国A股市场的有效性:

查看全部

先看一下这25个回归的判定系数R2,它度量了拟合程度的好坏。

1
R2inf1=pd.DataFrame(R2_1.reshape(5,5))
2
R2inf1.columns=['small_BE/ME','1','2','3','big_BE/ME']
3
R2inf1.index=['small_size','1','2','3','big_size']
4
R2inf1
查看全部
  small_BE/ME 1 2 3 big_BE/ME
small_size 0.594353 0.714150 0.350486 0.824708 0.825360
1 0.716048 0.731942 0.791529 0.828343 0.859161
2 0.718364 0.745072 0.884344 0.860049 0.804825
3 0.745227 0.870885 0.906794 0.895085 0.878805
big_size 0.795297 0.894826 0.834521 0.783604 0.786575

25个回归的R2大多处于0.7~0.9之间,已经是比较好的结果了,这点可以和其它的回归模型对比。

下面看一下市场因子的系数β

查看全部
 small_BE/ME123big_BE/ME
small_size1.0145000.9877961.3413761.0549651.035088
10.9752731.0162361.0239921.1030411.083893
20.9216800.9545281.0828091.0902981.036224
30.9604861.0414431.0691081.0137541.084644
big_size0.9067790.9607050.9594960.9481840.853405

我们可以看到β大多处于1左右。下面我们来检验其显著性,回归系数的显著性检验用的是t检验:原假设为t=0,若t统计量的值大于给定显著水平下的t分位值,则拒绝原假设,说明该系数显著大于0

1
import scipy.stats as stats
2
t107=stats.t.isf(0.025,106)  #自由度为n-p,显著水平5%下的t分位值
3
t107
查看全部
1.9825972617102912
查看全部
 small_BE/ME123big_BE/ME
small_size12.46238516.2734217.56299322.33174522.382173
116.34939417.01282620.06147622.61658225.428921
216.44301217.60121828.46945925.52264320.906988
317.60841926.73894132.11332830.07227427.724048
big_size20.29340330.03089823.12065619.59187819.765164

我们可以看到所有回归里β的t统计量的值都大于临界值,我们应该拒绝原假设,即表明β系数显著

以上,说明资本资产定价模型是有效的,市场因子的影响是显著的

2.回归二

如上,我们检验了CAPM模型的有效性,现在我们不妨检验一下另外两个因子对股票超额收益的解释作用。\begin{align}R(t)-R_f(t) = a+sSMB(t)+hHML(t)+e(t)\end{align}SMB和HML分别代表规模(市值)因子和账面市值比因子。

1
#作25次回归
2
import numpy as np
3
from sklearn import linear_model
4
a2=np.zeros(25)   #a项
5
s2=np.zeros(25)   #规模因子项系数
6
h2=np.zeros(25)   #账面价值比项系数
7
e2=np.zeros((25,108))   #残差项
8
R2_2=np.zeros(25)   #R2相关系数平方
9
ta2=np.zeros(25)
10
tb2=np.zeros(25)
11
import statsmodels.api as sm
12
from statsmodels.sandbox.regression.predstd import wls_prediction_std
13
ap2=np.zeros(25)  #a显著性检验的P值,下面类同
14
sp2=np.zeros(25)
15
hp2=np.zeros(25)
16
for i in range(25):
17
    X=np.zeros((2,108))
18
    X[0]=SMB
19
    X[1]=HML
20
    X=X.T
21
    X = sm.add_constant(X,has_constant='skip')
22
    y=EReturn[i]
23
    model = sm.OLS(y, X)
24
    results = model.fit()
25
    ap2[i]=results.pvalues[0]
26
    sp2[i]=results.pvalues[1]
27
    hp2[i]=results.pvalues[2]
28
    a2[i] = results.params[0]
29
    s2[i] = results.params[1]
30
    h2[i] = results.params[2]
31
    R2_2[i] = results.rsquared
32
    e[i] = results.resid
33
    tb2[i] = results.tvalues[1]
34
    ta2[i] = results.tvalues[0]
查看全部

同样,我们看一下25个回归的判定系数R2的情况:

查看全部
 small_BE/ME123big_BE/ME
small_size0.4292060.3938040.1214990.2985280.280941
10.3737880.3877740.2917210.2919670.232686
20.3982900.3193920.2229780.2079270.249022
30.2429460.1901580.1522700.1617530.129395
big_size0.1161280.0058020.0044710.0624310.124630

从R2可以看到,基本都在0.5以下,这个回归的结果比回归一差了很多,这个模型并不好,也就是说只用市值因子和账面市值比因子来解释股票超额收益是不合适的

3.回归三

这里的回归模型就是我们经典的三因子模型

\begin{align}R(t)-R_f(t) = a+b(R_M(t)-R_f(t))+sSMB(t)+hHML(t)+e(t)\end{align}

R(t)Rf(t):市场因子

SMB(t):规模(市值)因子

HML(t) :账面市值比因子

1
#作25次回归
2
import numpy as np
3
from sklearn import linear_model
4
a=np.zeros(25)   #a项
5
b=np.zeros(25)   #市场因子项系数
6
s=np.zeros(25)   #规模因子项系数
7
h=np.zeros(25)   #账面价值比项系数
8
e=np.zeros(25)   #残差项
9
R2=np.zeros(25)   #R2相关系数平方
10
for i in range(25):
11
    x=np.zeros((3,108))
12
    x[0]=MF
13
    x[1]=SMB
14
    x[2]=HML
15
    y=EReturn[i]
16
    x=np.mat(x).T
17
    y=np.mat(y).T
18
    regr = linear_model.LinearRegression()
19
    regr.fit(x,y)
20
    b[i]=regr.coef_[0][0]
21
    s[i]=regr.coef_[0][1]
22
    h[i]=regr.coef_[0][2]
23
    a[i]=regr.intercept_[0]
24
    e[i]=regr.residues_
25
    R2[i]=regr.score(x,y)
查看全部
查看全部

我们先看一下回归的R2:

1
R2inf3=pd.DataFrame(R2.reshape(5,5))
2
R2inf3.columns=['small_BE/ME','1','2','3','big_BE/ME']
3
R2inf3.index=['small_size','1','2','3','big_size']
4
R2inf3
查看全部
  small_BE/ME 1 2 3 big_BE/ME
small_size 0.850504 0.932359 0.401501 0.952128 0.942172
1 0.928505 0.947654 0.928649 0.949936 0.943393
2 0.960149 0.924252 0.962277 0.926240 0.903547
3 0.916107 0.945470 0.944791 0.933945 0.924949
big_size 0.932492 0.929273 0.901979 0.933192 0.942885

我们可以看到R2基本上都在0.9以上,三因子模型的拟合程度非常好,说明三因子模型是比CAPM更有效的模型

4.回归结果分析

如上三因子模型的有效性已经得到验证,确实能够解释股票收益来源,那么A股市场对这些因子有什么偏好呢?比如:长期来看,小盘股跑赢大盘股,还是大盘股能跑赢小盘股呢?

我们可以从第一个回归(CAPM)的截距项找到答案:

1
ainf1=pd.DataFrame(a1.reshape(5,5))
2
ainf1.columns=['small_BE/ME','1','2','3','big_BE/ME']
3
ainf1.index=['small_size','1','2','3','big_size']
4
tainf1=pd.DataFrame(ta1.reshape(5,5))
5
tainf1.columns=['small_BE/ME','1','2','3','big_BE/ME']
6
tainf1.index=['small_size','1','2','3','big_size']
7
print '第一个回归的a值:'
8
print ainf1.to_html()
9
print '自由度为n-p,显著水平5%下的t分位值:'+str(stats.t.isf(0.025,106))
10
print '第一个回归的a的t检验统计量:'
11
print tainf1.to_html()
查看全部
第一个回归的a值:
  small_BE/ME 1 2 3 big_BE/ME
small_size 0.024177 0.021577 0.041768 0.021694 0.018538
1 0.018026 0.021162 0.022099 0.020991 0.015612
2 0.015455 0.014398 0.016052 0.015725 0.019676
3 0.018244 0.010911 0.013619 0.012405 0.008653
big_size 0.005011 -0.000388 0.004878 0.006406 0.004192
自由度为n-p,显著水平5%下的t分位值:1.98259726171第一个回归的a的t检验统计量:
  small_BE/ME 1 2 3 big_BE/ME
small_size 3.101816 3.712481 2.459500 4.795915 4.186454
1 3.156042 3.699929 4.521662 4.495019 3.825185
2 2.879519 2.772733 4.407769 3.844395 4.145978
3 3.493139 2.925629 4.272353 3.843262 2.309965
big_size 1.171278 -0.126530 1.227596 1.382485 1.013901

可以看到25个回归里只有最后5个回归的a值是显著为0的,其余a值我们可以认为是不显著为0的,也就是说超额收益没有被完全解释,这也是三因子模型存在的必要。

我们从市值的角度来分析一下,以上结果,从上到下组合的市值越来越大,a值越来越小,a代表的是超额收益,也就是说市值越小的股票越容易获得超额收益,这点和我们的认知相同

从左到右,组合的账面市值比越来越大,a值从趋势上是越来越大,但也有很多反转,也就是说账面市值比越高的组合越容易获得超额收益这个结论并不准确。

四.总结

通过对三个模型的回归进行对比,我们可以看到,从R2来看,拟合结果最差的是第二个模型(只用市值因子和账面市值比),拟合结果最好的是第三个模型,也就是fama三因子模型。

综上,我们验证了Fama三因子模型在中国A股市场是有效的,也印证了市值小的股票更容易获得超额收益这一点。

本帖重在过程,学习交流。


阅读更多
换一批