碧沼

V1

2022/11/01阅读:15主题:橙心

研报复现——开源金工——估值坏了怎么修?

研报复现系列(2) 开源量化评论(1)——估值因子坏了怎么修?

前言

本文参考开源证券2020年6月16日的研报《开源量化评论(1)——估值因子坏了怎么修?》对其研究过程和结论进行复现学习。复现金工研报是学习量化策略,训练编程能力的很好途径。本文基本成功复现该研报的结果。

如果您对我的内容感兴趣,欢迎后台私信进行交流。

1.研报概述

价值投资长期以来是市场备受关注的投资理念。像EP、BP等估值因子不论是量化投资还是主观投资都是关注的基础因子。然而近几年估值因子的表现并不优秀,在因子测试中可能也有所失效。基于这样的背景,这篇研报讨论了如何修正估值因子,得到一个较好的表现。

图1 文章背景
图1 文章背景
图2 EP因子的分组测试(10组多空 个人绘制)
图2 EP因子的分组测试(10组多空 个人绘制)
图3 PB因子的分组测试(10组多空 个人绘制)
图3 PB因子的分组测试(10组多空 个人绘制)

从图2和图3传统估值因子的分组测试累计收益曲线在近两年,组间排序和间距已经出现了混乱和收敛的现象。因此如何改善传统估值因子的表现,是具有意义的一项工作。

基于这一背景,研报的思想十分简单,但效果不错。构建估值变动因子。即

估值变动因子可以认为是对传统估值因子在变动趋势和速率上的调整,这一速率(1-R/G)与股票的盈利增速和价格增速密切相关,直观的解读是价格增速小于盈利增速的时候,投资者对于股票盈利的反应过于悲观,调整系数为正,R>G的时候价格增速大于盈利增速,调整系数为负,反应过于乐观。

估值变动因子 dEP 与估值 因子 EP 同样都是正向选股因子。不同的是,估值因子 EP 是静态价值投资的 理念,关注的是“哪些股票的估值目前是最便宜的?”;而估值变动因子 dEP 是动态 价值投资的理念,关注的是“哪些股票的估值正在变得越来越便宜? ——研报原文

2.数据来源

本文的数据来源是通过Ricequant获取的EPttm、PBttm数据,因子测试框架自己进行搭建,测试框架尚不完善暂不开源。

获取当前市场证券列表,去掉ST股、去掉上市未满180天的新股。

def get_stock(date=enddate):
    all_stock = all_instruments(type = 'CS', date =date)
    all_stock = all_stock[['order_book_id','industry_code','listed_date','special_type']]
    all_stock.loc[:,'listed_date'] = pd.to_datetime(all_stock.loc[:,'listed_date'].values)
    all_stock = all_stock[(all_stock['listed_date'] < pd.to_datetime(date)-datetime.timedelta(180))&(all_stock['special_type']=='Normal' )]
    return all_stock

获取因子数据,并与证券代码进行拼接,保留行业标签,以便后续进行行业中性化。

def getfactors(date = date):
    stocklist = get_stock(date = date)[['order_book_id','industry_code']]
    factor = get_factor(stocklist.order_book_id, ['ep_ratio_ttm''pb_ratio_ttm''book_to_market_ratio_ttm''market_cap'], date = date)
    factor = factor.reset_index().sort_values(by = 'order_book_id').set_index('date')
    factor.reset_index(inplace=True)
    factor = pd.merge(factor, stocklist, how='inner', on='order_book_id')
    return factor

exchange_calendars库提供了免费便捷的获取世界主要交易所交易日历的方法。 获取交易日历,从2010年1月1日至,2022年10月31日,以22个交易日为间隔。(每22个交易日近似间隔1个月),并动态获取当前的因子信息。

import exchange_calendars as xcals
xshg = xcals.get_calendar("XSHG")
tradedate = list(xshg.trading_index("2010-01-01""2022-10-31", period="1D", force=True))[::22]
data = pd.DataFrame()
from tqdm import tqdm
for date in tqdm(tradedate):
    factor = getfactors(date=date)
    data = pd.concat([data,factor],axis=0)

获取价格数据:

date = pd.unique(data.date)
def getprice(date):
    stock = data[data['date']==date].order_book_id.values
    price = get_price(stock, start_date=date, end_date=date, frequency='1d', fields='close', adjust_type='pre_volume', skip_suspended =False, market='cn', expect_df=True)
    return price
price_data = pd.DataFrame()
for i in tqdm(pd.unique(data.date)):
    price_data = pd.concat([price_data,getprice(i)])

获取到价格数据后,我们还应该生成DEP60因子,由于选取的22个交易日间隔,60天两个月实际上是滞后两期的计算。DEP60因子生成:

data.reset_index(drop=True,inplace=True)
f_dep60 = data.groupby('order_book_id')['ep_ratio_ttm'].diff(2)
data.loc[:,'dep60'] = f_dep60

获得价格后,同样还要生成每个月的收益情况。

price_data.sort_index(inplace=True)
price_data.reset_index(inplace=True)
price_data.groupby('order_book_id').close.pct_change(1).fillna(0).shift(-1)

至此我们完成了所有的数据准备工作,接下来进行本地的因子测试。

3.因子测试

首先对因子分布进行观测。

图4 因子分布
图4 因子分布

非常直观的可以看到,分布由于存在尾部极端值,放大了图像,导致了整体的核密度曲线的观测呈现出了近似一条直线,我们应进行缩尾处理。

图5 因子分布(缩尾处理)
图5 因子分布(缩尾处理)

接着对因子进行自相关测试,可以看到自相关系数基本都大于0,表现出了该因子具有一定的动量效应。

图6 因子自相关测试
图6 因子自相关测试

对IC进行测试,IC大于0.05,ICIR大于2,因子的表现较好。 图7 IC测试

因子IC半衰期,由于是在财务指标上融入了价量指标,和预期的一致,因子的IC半衰期只有1个月。

图8 因子半衰期
图8 因子半衰期

本文还对因子进行行业中性化,由于dep因子本身已经包含了市值信息,因此就不再进行市值中性化。因子分组测试表现如下:

图9 因子分组测试表现
图9 因子分组测试表现

近似实现了研报原文的因子结果,具体数据展示如下,多空对冲组合躲过了15年股灾的大跌,回撤和夏普表现优异。

L-S多空对冲组合 dep表现最优组合
累积净值 5.99 7.92
年化收益 15.41% 18.01%
夏普比率 1.25 0.49
最大回撤 -11.60% -41.20%
最大回撤开始时间 2014-09-22 00:00:00 2015-05-14 00:00:00
最大回撤结束时间 2014-11-28 00:00:00 2015-12-28 00:00:00
年化收益/回撤比 1.33 0.44

5.总结

本文对【开源证券——估值因子坏了怎么修?】的研究流程基本进行了复现,完成了主体的因子测试部分,由于种种原因没有模仿原文对分行业、没有与传统因子的表现做细致的对比。这是本文的主要不足。

欢迎在后台进行交流,因子测试框架完善后会考虑开源。

分类:

人工智能

标签:

数据挖掘

作者介绍

碧沼
V1