01
三因子回归模型
Fama-French三因子回归是量化中最经典的模型之一,最早提出是在论文《Common risk factors in the returns on stocks and bonds》中,FAMA三因子回归模型可表示如下
其中,rt为投资组合的收益率,rf为无风险收益率,SMB为规模因子,HML为账面市值比因子,MKT为市场因子。
Fama-French三因子回归通过计算上述的三个因子,对股票的收益来源进行了分解。本文基于这篇论文,在A股上实现Fama-French三因子回归全流程。
论文及源码数据的获取方式见文末
。
02
解释变量
解释变量为三个因子SMB、HML、MKT,先贴上论文中因子定义原文,当然如果你不想看英语,可以跳过看后面我给的说明。
股票按规模分组
股票按账面市值比分组
分组后计算SMB、HML
MKT定义
总结一下
每年五月末,将股票按市值等分为两组Big(B)、Small(S),将账面市值比按30%、40%、40%分为三组Low(L)、Middle(M)、High(H)。上述分组组合之后可以得到六个组合:B/L、B/M、B/H、S/L、S/M、S/H。作者提到之所以规模分两组,账面市值比分三组是考虑到账面市值比的效果更强(黄色部分)。
计算这六个组合的市值加权收益率(Value-weighted return) ,作者提到计算市值加权收益率一方面是为了最小化方差,另一方面可以捕捉到大市值和小市值股票的不同特征。
HML、SMB因子定义如下
公式中左边代表每个组合的市值加权收益率,HML、SMB分别刻画了规模因子和账面市值比因子的风险溢价。
MKT因子定义为RM-RF,其中RM为全市场股票市值加权收益率,RF是无风险利率
以上是解释变量的定义。
03
被解释变量
被解释变量为投资组合的收益率,作者使用doublesort的方法构建了25个投资组合(关于doublesort可以看往期推文:
因子评估——双重排序
)。
还是先给出论文的定义
总结一下
,其实和前文自变量分组的方式是一样的,每年的5月末进行分组,只不过这一次对市值和账面市值比都分别分成5等分,组合之后得到25个投资组合,并计算这25个投资组合的市值加权收益率,作为因变量。
以上就是本文模型部分的全部说明,论文中还讨论了一些其他处理细节,有兴趣可以看看。
在定义好了自变量和因变量之后,就可以做25次回归,对结果进行分析。接下来用A股数据进行实证分析。
04
FF3因子的A股实证
先说明使用的数据
HML、SMB、因变量:使用2009年-2019年全A股月度数据进行计算(用其他频率也可)
MKT:MKT的计算比较简单,直接使用中国资产管理研究中心提供的数据了,当然如果想自己算的话,RM可以考虑用中证全指的收益率,RF可以用10年期国债到期收益率。
数据格式如下,如果你有其他数据源,处理成如下的形式,可以直接使用本文的代码
价格数据
估值数据
市值数据
MKT因子
接下来是实证部分,首先把账面市值比BM和市值mkt数据拼在一起,然后剔除新股和ST股,让结果稳健一些,当然也可以不剔。
price = pd.read_csv(
'复权价格.csv')
ST = pd.read_csv(
'ST.csv')
pb = pd.read_csv(
'PB.csv')
mkt = pd.read_csv(
'mkt.csv')
ipodate = pd.read_csv(
'上市日期.csv')
# 日期处理
price[
'tradedate'] = pd.to_datetime(price.tradedate)
ST[
'entry_dt'] = pd.to_datetime(ST.entry_dt)
ST[
'remove_dt'] = pd.to_datetime(ST.remove_dt)
mkt[
'tradedate'] = pd.to_datetime(mkt.tradedate)
pb[
'tradedate'] = pd.to_datetime(pb.tradedate)
ipodate[
'ipodate'] = pd.to_datetime(ipodate.ipodate)
BM = pb.
copy()
BM[
'BM'] =
1/BM.pb
BM = BM.
drop([
'pb'],axis =
1)
f = pd.merge(BM,mkt,left_on = [
'tradedate',
'stockcode'],right_on = [
'tradedate',
'stockcode'])
# 踢PB为负的
f =
f.
loc[
f.BM >
0].reset_index(
drop = True)
# 踢新股:上市不满一年
ipodate[
'entry_date'] = ipodate.ipodate + datetime.timedelta(
365)
f = pd.merge(
f,ipodate,left_on = [
'stockcode'],right_on = [
'stockcode'])
f =
f.
loc[
f.tradedate >=
f.entry_date].reset_index(
drop = True)
f =
f.
drop([
'ipodate',
'entry_date'],axis =
1)
# 剔ST
res =[]
for dates in
f.tradedate.unique(): # dates =
f.tradedate.unique()[
10]
fuse =
f.
loc[
f.tradedate == dates]
st_use = ST.
loc[ST.entry_dt <= dates]
st_use = st_use.
loc[(st_use.remove_dt > dates) | pd.isnull(st_use.remove_dt)]
scode_notst =
set(fuse.stockcode).difference(
set(st_use.stockcode))
fuse = fuse.set_index([
'stockcode']).
loc[scode_notst].reset_index()
res.
append(fuse)
res = pd.concat(
res,axis =
0)
f =
res.reset_index(
drop = True)
接下来生成6个投资组合:每年5月末,按市值分为两组,按账面市值比生成三组。以上过程通过函数
split_SIZE
和
split_BM
实现,通过
apply
和
groupby
得到每一期的分组。
f[
'ym'] = f.tradedate.apply(
lambda x:x.year*
100 + x.month)
f_5 = f.loc[f.ym %
10 ==
5].copy()
def split_BM(x):
x.loc[x[
'BM'] >= x.BM.quantile(
0.7),
'group_BM'] =
'H'
x.loc[x[
'BM'] < x.BM.quantile(
0.3),
'group_BM'] =
'L'
return x
def split_SIZE(x):
x.loc[x[
'mkt'] >= x.mkt.median(),
'group_SIZE'] =
'B'
return x
f_5[
'group_BM'] =
'M'
f_5 = f_5.groupby([
'ym']).apply(split_BM)
f_5 = f_5.reset_index(drop =
True)
f_5[
'group_SIZE'] =
'S'
f_5 = f_5.groupby([
'ym']).apply(split_SIZE)
f_5 = f_5.rename(columns = {
'ym':
'portfolio_dates'})
f_5 = f_5[[
'stockcode',
'portfolio_dates',
'group_BM',
'group_SIZE']]
f[
'portfolio_dates'] = f.tradedate.apply(
lambda x:x.year*
100 +
5
if x.month >
5
else (x.year -
1)*
100 +
5)
f = pd.merge(f,f_5,left_on =[
'stockcode',
'portfolio_dates'],right_on =[
'stockcode',
'portfolio_dates'])
f = f.reset_index(drop =
True)
f[
'portfolio_name'] = f.group_SIZE +
'/' + f.group_BM
运行结果如下
其中,portfolio_name代表股票对应的组别。以2009年6月为起点,计算这六个组合的市值加权收益率
# 计算六个组合的月度value weighted
return
ret = price.pivot(
index =
'tradedate',columns =
'stockcode',
values =
'price').pct_change(
1).shift(-
1).fillna(
0)
ret =
ret.stack().reset_index()
ret =
ret.
rename(columns = {
ret.columns[
2]:
'ret'})
sdate = datetime.date(
2009,
5,
1)
f =
f.
loc[
f.tradedate >= sdate].reset_index(
drop = True)
f = pd.merge(
f,
ret,left_on =[
'stockcode',
'tradedate'],right_on =[
'stockcode',
'tradedate'])
f.
loc[
f.tradedate == datetime.date(
2009,
5,
27),
'ret'] =
0
port_ret =
f.groupby([
'tradedate',
'portfolio_name']).apply(lambda
x:(
x.
ret*
x.mkt).sum()/
x.mkt.sum())
port_ret = port_ret.reset_index()
port_ret = port_ret.
rename(columns = {port_ret.columns[-
1]:
'ret'})
看一下六个组合收益曲线
明显可以看出,小市值股票(S)表现更好,低账面市值比(L)表现最差,接下来计算SMB、HML因子,并和MKT因子拼在一起。
# SMB因子
# SMB = 1/3(SL + SM + SH) - 1/3(BL + BM + BH)
SMB = (port_ret_pivot[
'S/L'] + port_ret_pivot[
'S/M'] + port_ret_pivot[
'S/H'])/
3 - (port_ret_pivot[
'B/L'] + port_ret_pivot[
'B/M'] + port_ret_pivot[
'B/H'])/
3
# HML因子 HML = (SH + BH)/2 - (SL + BL)/2
HML = (port_ret_pivot[
'S/H'] + port_ret_pivot[
'B/H'])/
2 - (port_ret_pivot[
'S/L'] + port_ret_pivot[
'B/L'])/
2
ff3 = pd.concat([SMB,HML],axis =
1)
ff3 = ff3.reset_index()
ff3.columns = [
'tradedate',
'SMB',
'HML']
ff3[
'ym'] = ff3.tradedate.apply(
lambda x:x.year*
100 + x.month)
# RM:全市场流通市值加权指数收益率
# RF:无风险利率
# 这里直接用中国资产管理研究中心的数据
d = pd.read_csv(
'fivefactor_monthly.csv')
RM_RF = d[[
'trdmn',
'mkt_rf',
'rf']].copy()
RM_RF = RM_RF.rename(columns = {
'trdmn':
'ym'})
ff3 = pd.merge(ff3,RM_RF,left_on = [
'ym'],right_on = [
'ym'])
看看每个因子的累计收益率
三因子的相关性如下
可以看出,因子相关性不高,适合做回归。
接下来构造因变量25个投资组合,过程和上面类似。
groups =
5
f_5 =
f.
loc[
f.ym %
10 ==
5,[
'stockcode',
'tradedate',
'ym',
'BM',
'mkt']].
copy()
f_5[
'g_BM'] = f_5.BM.groupby(f_5.tradedate).apply(lambda
x:np.
ceil(
x.rank()/(
len(
x)/groups)))
f_5[
'g_SIZE'] = f_5.mkt.groupby(f_5.tradedate).apply(lambda
x:np.
ceil(
x.rank()/(
len(
x)/groups)))
f_5 = f_5.
rename(columns = {
'ym':
'portfolio_dates'})
f_5 = f_5[[
'stockcode',
'portfolio_dates',
'g_BM',
'g_SIZE']]
f[
'portfolio_dates'] =
f.tradedate.apply(lambda
x:
x.year*
100 +
5
if
x.month >
5
else (
x.year -
1)*
100 +
5)
f = pd.merge(
f,f_5,left_on =[
'stockcode',
'portfolio_dates'],right_on =[
'stockcode',
'portfolio_dates'])
f =
f.reset_index(
drop = True)
这25个组合的平均市值
从上往下,SIZE增大,从左往右,BM增大。平均市值从上往下依次增大,可以验证分组没有算错。
每个组别的股票个数如下
接下来这张:每个分组的平均收益率
这张可以分析出很多信息,实际上是一个标准的doublesort操作。从上往下,市值增大,平均收益减小。从左往右,账面市值比增大,平均收益也增大。
接下来生成这25个组合的收益率数据,用于回归,数据格式如下
横轴25行代表25个组合,纵轴代表时间,累计收益率如下
将25个组合数据和前面生成的三因子数据合并,进行三因子回归,记录回归的beta、p值、t值、r2。
f25 = pd.merge(f25,ff3,left_on = [
'tradedate'],right_on = [
'tradedate'])
f25[
'Intercept'] =
1
x = f25.
loc[:,[
'SMB',
'HML',
'mkt_rf',
'Intercept']].
values
r2 = []
betas = []
t = []
p = []
for i in
range(
25):# i =
0
y = f25.
loc[:,f25.columns[i+
1]].
values
mod =
sm.OLS(
y,
x).fit()
r2.
append([f25.columns[i+
1],
mod.rsquared])
betas.
append([f25.columns[i+
1]] +
list(
mod.params))
t.
append([f25.columns[i+
1]] +
list(
mod.tvalues))
p.
append([f25.columns[i+
1]] +
list(
mod.pvalues))
p = pd.DataFrame(
p,columns = [
'group',
'SMB',
'HML',
'mkt_rf',
'Intercept'])
t = pd.DataFrame(t,columns = [
'group',
'SMB',
'HML',
'mkt_rf',
'Intercept'])
betas = pd.DataFrame(betas,columns = [
'group',
'SMB',
'HML',
'mkt_rf',
'Intercept'])
r2 = pd.DataFrame(r2,columns = [
'group',
'r2'])
运行结果如下,
首先看R2
对于金融数据来说,算是比较高的水平了,拟合的还不错。t值和p值代表的含义差不多,看p值更直观一些。
记录了每次回归每个系数的p值,可以看出,规模因子HML非常显著,几乎每次回归都显著,其他因子的显著性一般。
接下来看beta
beta是回归的系数,这里能看出来,MKT、SMB的系数为正,HML系数有负有正,前面p值只有SMB是非常显著的,并且SMB的正系数也是符合常理的。
这里需要细致分析的是截距项,把截距项转换成上面双重排序的格式来看
截距项表示的是股票收益中不能被三个因子解释的部分,也就是alpha的部分。从截距项的大小来看,也是随着SIZE增大,alpha减小,随着BM增大,alpha增大。表明小市值的股票和高账面市值比的股票更容易获得超额回报,非常符合常理。
除此外,也可以对残差项进行分析,这里略去。
以上是Fama三因子模型的全过程,之后会有一些基于Fama三因子的因子构建,请关注。
05
参考文献
Fama E F, French K R. Common risk factors in the returns on stocks and bonds[J]. Journal of, 1993.
觉得不错就点个好看吧