一、概述

Fama-French三因子模型(以下简称“三因子模型”)是法玛和法兰奇在1990年代初提出来的,它认为理想状态下,资产的超额收益由市场收益、规模收益、价值收益三个部分组成。对应的公式是:

E(R(t))=Rf(t)+bE(RM(t)−Rf(t))+sE(SMB(t))+hE(HML(t))E(R(t))=R_f(t)+bE(R_M(t)-R_f(t))+sE(SMB(t))+hE(HML(t))E(R(t))=Rf​(t)+bE(RM​(t)−Rf​(t))+sE(SMB(t))+hE(HML(t))

其中,
RMR_MRM​ 是市场因子,
SMBSMBSMB 是规模因子,
HMLHMLHML 是价值因子。

二、因子构成与回归过程

1、原版三因子模型

原文中分别对股票和债券做了探索,我们这里只关心股票,债券暂不涉及。
1)股票选择
从1963年至1991年,所有NYSE、Amex、NASDAQ上市,并且上市时间超过2年的股票,同时剔除上一年年报中所有者权益为负的股票。作者提到,上一年年报中所有者权益为负的股票在1980年之前很少,认为剔除的影响不大。

2)股票组合——分组
a、将入选股票按每年6月的普通股市值从大到小排序,大于50%分位的归到B(big)组,其余归到S(small)组。
b、剔除普通股市值不正常的股票,即 ADRs、REITs 和受益权。
c、将入选股票按照上年末的账面市值比的大小排序(实际数据也是在次年6月末公布),按30%、70%两个分位,分成三组 L(low,< 30%)、M(medium,[30%,70%])、H(high,>70%)。
d、账面市值比 = 上一年年报中的所有者权益 / 除以上一年年末的普通股市值。

3)股票组合——取交集
将所有又在B组,又在L组的股票分到BL组中,以此类推,将所有股票都分别分到 BL、BM、BH、SL、SM、SH 六个组中。

4)因子值计算
a、SMB、HML
在每年6月底确认了6个股票组合后,从7月份到第二年6月份,组合都不变,逐月计算6个组合的月市值加权收益率。
因子值

SMB=SL+SM+SH3−BL+BM+BH3SMB=\frac{SL+SM+SH}{3}-\frac{BL+BM+BH}{3}SMB=3SL+SM+SH​−3BL+BM+BH​

HML=SH+BH2−SL+BL2HML=\frac{SH+BH}{2}-\frac{SL+BL}{2}HML=2SH+BH​−2SL+BL​

b、市场收益率
所有入围股票的市值加权月收益率,就是市场收益率。

c、无风险收益率
期限一个月的美国国债的收益率。

5)被解释变量
将股票在每年6月份,分别按市值、价值(账面市值比)大小分成5组,交叉取交集,得到25组股票组合,每个组合计算市值加权月收益率序列。

6)回归与检验
在1963年至1991年的数据上重复以上过程,共得到342个月的三因子收益率,以及25组组合的月收益率,将这25组组合的收益率逐组与三因子收益率进行时间序列回归,并检验其结果。
由此得出一堆结论,这里不赘述。

2、修改版三因子模型

根据中国国内股票市场的数据情况,将原版模型做一定的简化修改,具体如下:
(嗯,为方便起见,有点简单粗暴 #_#)
1)股票选择
选取沪深股市从2017年至2021年,所有上市股票,不作任何剔除。

2)股票组合——分组
a、将入选股票的流通市值在每个交易日从大到小排序,大于50%分位的归到B(big)组,其余归到S(small)组,每天分组。
b、将入选股票的账面市值比( = 1 / 动态 PB) 在每个交易日按从大到小排序,按30%、70%两个分位,分成三组 L(low,< 30%)、M(medium,[30%,70%])、H(high,>70%)。

3)股票组合——取交集
将所有又在B组,又在L组的股票分到BL组中,以此类推,将所有股票都分别分到 BL、BM、BH、SL、SM、SH 六个组中。

4)因子值计算
a、SMB、HML
分别计算指定周期下,6个组合的市值加权收益率,即为其因子值。
因子值

SMB=SL+SM+SH3−BL+BM+BH3SMB=\frac{SL+SM+SH}{3}-\frac{BL+BM+BH}{3}SMB=3SL+SM+SH​−3BL+BM+BH​

HML=SH+BH2−SL+BL2HML=\frac{SH+BH}{2}-\frac{SL+BL}{2}HML=2SH+BH​−2SL+BL​

b、市场收益率
将中证800指数的月收益率视为市场收益率。
(就是这么任性,有批评的话,虚心接受…死不悔改 #_#)

c、无风险收益率
3.2%的年化收益率,按指定周期折算。

5)被解释变量
随便选定了几只股票,算出其指定周期下的收益率。

6)回归与检验
将每只股票的指定周期(因为数据时间较短,因此选定为周)的收益率,分别与相同周期下的三因子收益率进行时间序列回归,并展示回归结果。

三、改进之处

要改进的地方嘛,很多,比如:
1)股票选择。似乎不应该把 st类、上一年资不抵债的股票、次新股选入。
2)股票分组依据。似乎没必要每日都分组,在关键时间点上分就好了嘛。
3)股票分组依据。账面市值比的计算,咱别每天都算,就按原版的一年一次,或者跟着财报发布频率走成不?太频繁了晃得我眼花。。。。。。
4)市场收益率。全市场选股这么能选中证800,没有代表性,不是有一堆全A指数吗。
5)被解释变量。为啥选个股啊,如果拟合优度不超过0.5,小心被打脸吧。
6)balabalabala。。。。。。

嗯,都说到我心里去了,虚心接受,不过,死不悔改。。。。。好吧,这次就不改了,下一版或者下个模型时再说吧。

四、talk is cheap,show me the code

最后,把代码放出来,如果哪位骨骼清奇的小伙伴感兴趣,也可以自己按上边的改进方向改改。

环境:
1)win10 + anaconda 4.8.4
2)pycharm 2020.3.2 CE
3)数据库:sqlite3,数据已经放入

# -*- coding: utf-8 -*-
import pandas as pd
import numpy as np
import xcsc_tushare as xcts
import seaborn as sns
import matplotlib as mpl
import statsmodels.api as sm
import sqlite3 as sqlfrom pyecharts import options as opts
from pyecharts.charts import Radar
from os.path import exists
from tqdm import tqdm
from typing import List, Tuple, Dict
from pickle import dump
from gc import collectsns.set()
# mpl.rcParams['font.sans-serif'] = ['SimHei']
mpl.rcParams['font.sans-serif'] = ['Microsoft YaHei']  # 指定默认字体:解决plot不能显示中文问题
mpl.rcParams['axes.unicode_minus'] = False  # 解决保存图像是负号'-'显示为方块的问题token_xcts_prd = 'your token'
xcts.set_token(token_xcts_prd)
xpro = xcts.pro_api(env='')
def get_trading_dates(cur_conn: any, sdate: str, edate: str) -> pd.DataFrame:"""取指定起止日期的交易日"""with cur_conn:cur_sql = 'select * from trade_date'cur = cur_conn.cursor()cur.execute(cur_sql)rs = cur.fetchall()print('读取交易日数据')col_name = [ii[0] for ii in cur.description]tdates = pd.DataFrame(rs, columns=col_name)return tdates
def get_dates_week(tdates: any, is_end: bool = True, is_cut: bool = False) -> List:"""从输入的日期序列中,按要求抽取指定频率的最后一天的日期出来,比如 'w' 是抽取每周最后一天参数说明:is_end: 取每个周期的第一天,还是最后一天is_cut: 如果时间序列不够整周期,是否要补充头尾日期。 is_cut = True 是补上比如取月频交易日的第一天的情况,如果时间序列的首个日期不是首个完整月的第一天,则补上或者取月频交易日最后一天的情况,如果时间序列的末个日期不是末个完整月的最后一天,则补上"""if isinstance(tdates, List):tdates_df = pd.to_datetime([str(ii) for ii in tdates]).to_frame().reset_index(drop=True)tdates_df.columns = ['trade_date']elif isinstance(tdates, pd.Series):tdates_df = pd.to_datetime(tdates.astype(str)).to_frame()tdates_df.columns = ['trade_date']else:tdates_df = tdates['trade_date'].copy().to_frame()# 通过比较某一日及其前后日是周几,来判断该日是否周末# 只要发生后一天的 weekday 不大于前一天的,那前一天就一定是该周最后一天tdates_df['weekday'] = tdates_df['trade_date'].dt.weekday# 选出每周第一天,还是每周最后一天if is_end:weekend_index = tdates_df[tdates_df['weekday'] == 0].index - 1else:weekend_index = tdates_df[tdates_df['weekday'] == 0].indextdates2 = tdates_df.iloc[weekend_index]tdates_list = tdates2.iloc[:, 0].tolist()# 补充首末日期if not is_cut:if is_end:  # 取周期的末个日期,需要补上原日期序列的末位if tdates_list[-1] != tdates_df['trade_date'].iloc[-1]:tdates_list.append(tdates_df['trade_date'].iloc[-1])else:  # 取周期的首个日期,需要补上原日期序列的首位if tdates_list[0] != tdates_df['trade_date'].iloc[0]:tdates_list = [tdates_df['trade_date'].iloc[0]] + tdates_listreturn tdates_listdef calc_period_ret(raw_df: pd.DataFrame, cur_freq: str = 'w', is_end: bool = True,is_cut: bool = False) -> pd.DataFrame:"""将输入的 日收益率序列,计算指定频率的收益率不能直接用 .resample() 来改,因为它无法正确处理春节、五一、国庆等长假的情况"""ddates = get_dates(raw_df.index.tolist(), cur_freq, is_end, is_cut)ddates_df = pd.DataFrame(ddates, index=ddates)raw_df2 = pd.merge(raw_df / 100 + 1, ddates_df, left_index=True, right_index=True, how='left')# if raw_df.index[0] != ddates[0]:#     raw_df2.iloc[0, -1] = raw_df.index[0]raw_df2 = raw_df2.fillna(method='bfill')raw_df2 = raw_df2.groupby(0).prod()return raw_df2 - 1def calc_factors(raw_df: pd.DataFrame) -> Tuple[float, float]:"""单个周期具体计算smb、hml因子值"""df_ = raw_df.copy()# 划分大小市值公司df_['sb'] = df_['float_a_shr'].apply(lambda x: 'b' if x >= df_['float_a_shr'].median() else 's')# 求账面市值比df_['bm'] = 1 / df_['pb_new']# 划分高、中、低账面市值比公司border_down, border_up = df_['bm'].quantile([0.3, 0.7])df_['hml'] = df_['bm'].apply(lambda x: 'h' if x >= border_up else 'm')df_['hml'] = df_.apply(lambda row: 'l' if row['bm'] <= border_down else row['hml'], axis=1)# 组合划分为6组df_sl = df_[(df_['sb'] == 's') & (df_['hml'] == 'l')]df_sm = df_[(df_['sb'] == 's') & (df_['hml'] == 'm')]df_sh = df_[(df_['sb'] == 's') & (df_['hml'] == 'h')]df_bl = df_[(df_['sb'] == 'b') & (df_['hml'] == 'l')]df_bm = df_[(df_['sb'] == 'b') & (df_['hml'] == 'm')]df_bh = df_[(df_['sb'] == 'b') & (df_['hml'] == 'h')]# 计算各组收益率ret_sl = (df_sl['pct_chg'] * df_sl['float_a_shr']).sum() / df_sl['float_a_shr'].sum()ret_sm = (df_sm['pct_chg'] * df_sm['float_a_shr']).sum() / df_sm['float_a_shr'].sum()ret_sh = (df_sh['pct_chg'] * df_sh['float_a_shr']).sum() / df_sh['float_a_shr'].sum()ret_bl = (df_bl['pct_chg'] * df_bl['float_a_shr']).sum() / df_bl['float_a_shr'].sum()ret_bm = (df_bm['pct_chg'] * df_bm['float_a_shr']).sum() / df_bm['float_a_shr'].sum()ret_bh = (df_bh['pct_chg'] * df_bh['float_a_shr']).sum() / df_bh['float_a_shr'].sum()# 计算smb, hml并返回cur_smb = (ret_sl + ret_sm + ret_sh - ret_bl - ret_bm - ret_bh) / 3cur_hml = (ret_sh + ret_bh - ret_sl - ret_bl) / 2return cur_smb, cur_hmldef get_factors_data(dpath: str, cur_conn: any, tdates: pd.Series, cur_freq: str = 'w') -> pd.DataFrame:"""取相关数据,计算 smb、hml 因子"""# 按指定频率生成时间轴tdates_freq = get_dates_week(tdates=tdates, is_end=True, is_cut=False)# 计算指定时间轴上所有股票的收益率pct_chg_day = raw_data.pivot(index='trade_date', columns='ts_code', values='pct_chg').sort_index()pct_chg_freq = calc_period_ret(pct_chg_day, cur_freq, True, False)del pct_chg_day    # 变量用完之后值清除掉,释放内存collect()# 取指定频率日期的因子基础时点数据tdates_freq_str = [str(dt.year * 10000 + dt.month * 100 + dt.day) for dt in tdates_freq]raw_data_freq = raw_data[raw_data['trade_date'].isin(tdates_freq_str)].reset_index(drop=True)raw_data_freq.loc[:, 'trade_date'] = pd.to_datetime(raw_data_freq['trade_date'])del raw_data   # 变量用完之后值清除掉,释放内存collect()# 组合成包含指定频率的收益,和指定频率时点原始数据的 dataframepct_chg_freq = pct_chg_freq.stack().to_frame()pct_chg_freq.columns = ['ret_w']pct_chg_freq = pct_chg_freq.reset_index()pct_chg_freq.columns = ['trade_date', 'ts_code', 'ret_w']raw_data_freq = pd.merge(raw_data_freq, pct_chg_freq, on=['trade_date', 'ts_code'], how='left')raw_data_freq = raw_data_freq.drop('pct_chg', axis=1)raw_data_freq.rename(columns={'ret_w': 'pct_chg'}, inplace=True)raw_data_freq['pct_chg'] -= 1res_ser = raw_data_freq.groupby('trade_date').apply(calc_factors)res_df = pd.DataFrame(list(res_ser), index=res.index, columns=['smb', 'hml'])res_df.to_pickle(dpath + f'\\three_factors_{cur_freq}_df.pkl')return res_df
def get_ret_data(dpath: str, fdata: pd.DataFrame, stks_dict: Dict,cur_idx: Dict, sdate: str, edate: str) -> pd.DataFrame:print('计算资产回报数据')# 取指定股票和基准指数的数据stks_data_list = []for kk, vv in stks_dict.items():cur_data = xpro.daily(ts_code=kk, start_date=sdate, end_date=edate)cur_data.index = pd.to_datetime(cur_data.trade_date)stks_data_list.append(cur_data.pct_chg / 100)# 补上指数的cur_data = xpro.index_daily(ts_code=cur_idx['code'], start_date=sdate, end_date=edate)cur_data.index = pd.to_datetime(cur_data.trade_date)stks_data_list.append(cur_data.pct_chg / 100)stock_df = pd.concat(stks_data_list, axis=1)stock_df.columns = list(stks_dict.values()) + [cur_idx['name']]stock_df = stock_df.sort_index(ascending=True)# 折算出指定频率的收益率stock_freq_ret_df = calc_period_ret(stock_df * 100, cur_freq, True, False)# 算出超额收益率rf = 1.032 ** (5 / 360) - 1stock_freq_ret_df -= rfstocks_ret = pd.merge(stock_freq_ret_df, fdata, left_index=True, right_index=True, how='inner')stocks_ret = stocks_ret.fillna(0)stocks_ret.to_pickle(dpath + r'\stocks_ret.pkl')return stocks_ret
def plot_factors_radar(plt_data: pd.DataFrame, dpath: str) -> None:c_schema = [{"name": "alpha", "max": 0.01, "min": -0.01},{"name": "市场因子", "max": 2, "min": 0},{"name": "市值因子", "max": 1, "min": -1},{"name": "价值因子", "max": 1, "min": -1}]  # 以字典形式设置雷达图指标名称和范围cur_data = plt_data.values.T.tolist()  # 以字典形式设置雷达图指标名称和范围radar = Radar()  # 初始化对象,单独调用radar.add(series_name=plt_data.columns[0],data=[cur_data[0]],color="#f9713c",areastyle_opts=opts.AreaStyleOpts(opacity=0.1),linestyle_opts=opts.LineStyleOpts(width=3))  # 添加第一类数据并绘图radar.add(series_name=plt_data.columns[1],data=[cur_data[1]],color="#4169E1",areastyle_opts=opts.AreaStyleOpts(opacity=0.1),linestyle_opts=opts.LineStyleOpts(width=3))  # 添加第二类数据并绘图radar.add(series_name=plt_data.columns[2],data=[cur_data[2]],color="#00BFFF",areastyle_opts=opts.AreaStyleOpts(opacity=0.1),linestyle_opts=opts.LineStyleOpts(width=3))  # 添加第三类数据并绘图radar.add(series_name=plt_data.columns[3],data=[cur_data[3]],color="#3CB371",areastyle_opts=opts.AreaStyleOpts(opacity=0.1),linestyle_opts=opts.LineStyleOpts(width=3))  # 添加第四类数据并绘图radar.add_schema(schema=c_schema, shape="polygon")  # schema设置radar.set_series_opts(label_opts=opts.LabelOpts(is_show=False))  # 是否打标签radar.set_global_opts(title_opts=opts.TitleOpts(title="因子解释度"))  # 标题radar.render(dpath + r'\radar.html')  # 渲染成html格式
if __name__ == '__main__':# 参数start_date = '20170101'end_date = '20210118'data_path = r'e:\fama_french3\data'index_names = {'name': '中证800','code': '000906.SH'}stock_names = {'000002.SZ': '万科A','601318.SH': '中国平安','600519.SH': '贵州茅台','002415.SZ': '万华化学','002230.SZ': '科大讯飞'}conn = sql.connect(data_path + r'\xctushare.db')freq = 'w'# 取交易日trading_dates = get_trading_dates(conn, start_date, end_date)# 取指数成分股的因子数据与收益率数据factors_data = get_factors_data(data_path, conn, trading_dates['trade_date'], freq)# 取指定资产的原始数据,与指数成分股分组收益率的时间周期对齐后合并ret_data = get_ret_data(data_path, factors_data, freq, stock_names, index_names, start_date, end_date)# 观察数据间的相关性热力图# sns.heatmap(ret_period.iloc[:, :5].corr(), cmap='bwr')# # 收益率时序图# plt.figure(figsize=(10, 5))# for col in ret_period.columns:#     plt.plot(ret_period[col], label=col)# plt.title('日收益率时序图', fontsize=20)# plt.legend()## # 累计收益率时序图# plt.figure(figsize=(10, 5))# for col in ret_period.columns:#     plt.plot((ret_period[col]).cumprod()-1, label=col)# plt.title('累计收益率时序图', fontsize=20)# plt.legend()# 回归计算三因子载荷# res_model = {}ols_params = []for stock in stock_names.values():xx = sm.add_constant(ret_data[['中证800', 'smb', 'hml']].values)yy = ret_data[stock].valuesres = np.linalg.inv(xx.T.dot(xx)).dot(xx.T).dot(yy)ols_params.append(res)# result = sm.OLS(ret_data[stock], sm.add_constant(ret_data[['中证800', 'smb', 'hml']].values)).fit()# res_model[stock] = result# ols_params.append(result.params)# print(stock + '\n')# print(result.summary())# print('\n\n')# ols_params_df = pd.concat(ols_params, axis=1)ols_params_df = pd.DataFrame(ols_params).Tols_params_df.columns = stock_names.values()# 将以上股票的三因子载荷集中画在雷达图上,生成 .html 文档,需要手动打开plot_factors_radar(ols_params_df, data_path)

(后续将会陆续介绍其他因子模型,敬请期待,更新速度嘛…啊哈哈哈,今天天气真好…)

参考资料:

1、Fama, French, Common risk factors in the returns on stocks and bonds(1993)
2、https://zhuanlan.zhihu.com/p/55071842

Fama-French 三因子模型介绍、修改与框架搭建相关推荐

  1. mock介绍及moco框架搭建使用

    VOL 183 14 2020-12 今天距2021年17天 这是ITester软件测试小栈第183次推文 点击上方蓝字"ITester软件测试小栈"关注我,每周一.三.五早上 0 ...

  2. day68_淘淘商城项目_01_电商介绍 + 互联网术语 + SOA + 分布式 + 集群介绍 + 环境配置 + 框架搭建_匠心笔记

    淘淘商城项目_01 1.电商行业的背景介绍--电子商务 1.1.11.11 1.2.电商行业技术特点 2.淘淘商城的系统架构 2.1.淘淘商城介绍 2.2.功能介绍 2.3.系统架构 2.3.1.传统 ...

  3. 数据分析---Fama-French三因子模型

    大家好,今天带给大家一篇金融模型方面的python应用文章,在这篇文章中将会给大家介绍pandas和statsmodels.api,此外还会介绍Fama-French三因子模型的理论知识. 目录 Fa ...

  4. python量化:如何利用tushare构造FF三因子模型?

    Python量化:如何利用tushare构造FF三因子模型? FF三因子模型介绍 代码实现 从tushare调取数据 利用数据构建因子 总结 笔者是一枚大二菜狗,最近刚上完学院开的python金融量化 ...

  5. 一文教你看懂Fama-French三因子模型

    Fama-French三因子模型概述 Fama-French三因子模型(Fama-French 3-factor model,简称FF3) Fama和French 1992年对美国股票市场决定不同股票 ...

  6. 修改layui框架html,layui框架如何设置分页?(方法介绍)

    layui框架如何设置分页?下面本篇文章给大家介绍一下layui框架中设置分页的方法.有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助. 具体步骤如下: 1.从 官方文档 - 内置模块 ...

  7. fama matlab源码_基于优化算法改造的Fama-French三因子模型

    基于光大证券金融工程研报<站在巨人的肩膀上,从牛基组合到牛股发现 --FOF 专题研究系列之十六 >中提及的Carhart四因子Alpha优化模型,本文在Fama-French三因子模型上 ...

  8. Python量化交易06——Fama-French三因子模型(Rmt,SMB,HML)

    参考书目:深入浅出Python量化交易实战 本次带来的是著名的获得了诺贝尔奖的三因子模型. 因子模型介绍 Fama和French从可以解释股票收益率的众多因素中提取出了三个重要的影响因子,即市场风险溢 ...

  9. python量化策略——Fama-French三因子模型

    介绍:Fama-French三因子模型,是Fama和French 1992年对美国股票市场决定不同股票回报率差异的因素的研究发现,股票的市场的beta值不能解释不同股票回报率的差异,而上市公司的市值. ...

最新文章

  1. 李飞飞团队从动物身上get AI新思路,提出RL计算框架,让机器在复杂环境学习和进化...
  2. 【查看Linux带宽】nload、iptraf 的安装与使用
  3. tf rnn layer
  4. epoll 边沿触发 非阻塞 IO 服务器
  5. Homestead 使用总结
  6. axis2 java.net.url_axis,axis2调用.net的webservice
  7. 分布式离线计算—MapReduce—为什么被淘汰了?
  8. 基于块的linux驱动程序,基于块的Linux驱动程序 块设备驱动 centos内核编译过程 操作系统课程设计...
  9. PHP新手之学习类与对象(4)
  10. RemObjects SDK简介
  11. 围棋知名AI-KataGo 下载分享
  12. Realsense D435i 使用
  13. 桌面计算机图标无法显示属性,Win7系统桌面图标显示异常的解决方法大全
  14. C语言论坛系统课程设计
  15. 【调试工具】【tc】Linux流量控制原理【转】
  16. HTML空格符号 nbsp; ensp; emsp; 介绍以及实现中文对齐的方法
  17. matplotlib 绘制 虚线线型之间的间隔设置
  18. 2021年陕西省安全员C证考试内容及陕西省安全员C证考试资料
  19. Acme CAD Converter 的线宽要怎么设置啊
  20. 最新Win10离线安装.NET Framework 3.5的方法(不需要离线包,只需原版系统安装包,亲测完美快捷有效)

热门文章

  1. 上传资源到静态服务器
  2. Android开发调试神器——Stetho介绍
  3. jQuery国际化i18n--中文、英文、中文繁体
  4. 直线相交的交点(Line-Line Intersection)
  5. 使用bat脚本运行jar程序 cmd下解决乱码问题
  6. Pro_07丨波动率因子3.0与斜率因子
  7. c语言性能测试库,C语言qsort函数算法性能测试
  8. 测试环境:一种更准确的测试环境温度监测方法
  9. 【BI学习心得15-数据分析思维】
  10. 产品架构图到底是怎么“画”出来的?