大数跨境
0
0

【爬虫】python构建本地量化选股系统(二)——因子测试

【爬虫】python构建本地量化选股系统(二)——因子测试 Paper数据分析
2022-10-05
1
导读:爬取服务介绍 本号推出数据爬虫服务,客户可将需要爬取的网址,字段,具体需求等信息发送给下方客服,并进行定

爬取服务介绍

   本号推出数据爬虫服务,客户可将需要爬取的网址,字段,具体需求等信息发送给下方客服,并进行定制化爬取服务,具体收费标准根据难度商议协定。

如需定制,请添加客服1微信

   



上一篇文章,获取了全部行业的个股行情和财务数据,这构成了我本地量化系统的基础数据,当然,后面可能去爬取更多的数据进行扩充。

本文介绍如何利用这些基础数据进行指标构建和一键式指标回测,从而筛选出针对特定行业有效的因子,以便后面利用这些因子构造投资组合进行回测。

本次的测试针对医药制造行业,事实上,其他行业操作过程完全相同,只需读取各自行业不同的基础数据,即可完成相同的操作。其实之前也想过将所有行业的数据合成一张大表,并且使用虚拟变量表示不同的行业。这样做的优点是数据量明显增大,从而可以使用机器学习等算法,缺点则是获取的有效因子是针对全行业的,而对于特定行业的适用性或许不如对于各个行业单独测试的因子更为有效,并且数据处理过程繁琐,表的宽度可能因为加入过多虚拟变量而变得过宽。所以,窥一豹而见全身,本文仅介绍对于医药行业的因子测试,如果有其他行业的因子测试需求,可以私聊客服进行定制~

1、数据预处理

下面开始因子测试前的准备工作:数据整理。这里的思路是分别对行业内每一只股票相关表格进行整理,并进行纵向合并,生成一张总表all_data。后面进行回测时只需在总表中选择回测时间即可。

首先,读取基础数据,包括日度行情数据和财务报表数据,

for name in glob.glob('E:/医药制造/hisdata/*_hisdata.csv'):
coder = re.findall('\d{6}',name)
code.extend(coder) #不要用append
print(coder,len(code))
df_day = pd.read_csv('E:/医药制造/hisdata/'+str(coder)[2:8]+'_hisdata.csv')
df_lrb = pd.read_csv('E:/医药制造/lrb/'+str(coder)[2:8]+'lrb.csv')
df_xjllb = pd.read_csv('E:/医药制造/xjllb/'+str(coder)[2:8]+'xjllb.csv')
df_zcfzb = pd.read_csv('E:/医药制造/zcfzb/'+str(coder)[2:8]+'zcfzb.csv')
df_cwzb = pd.read_csv('E:/医药制造/cwzb/'+str(coder)[2:8]+'cwzb.csv')

由于行情数据df_day 为每日更新,而财务数据为每季度更新,所以需要想办法将两种数据进行合并。这里按照年份和季度生成变量date_id,使得df_day和财务数据具有相同的列名date_id,从而便于后面将行情数据和财务数据进行合并。由于每日的行情数据表和财务数据表具有不同结构,所以分别对行情数据和财务数据(以利润表df_lrb为例)进行日期处理,并生成date_id列。

df_day['年'] = df_day['日期'].apply(lambda x: x[:4])
df_day['月'] = df_day['日期'].apply(lambda x: x[5:7])
df_day['季度'] = df_day['月'].map({'01':1,'02':1,'03':1,'04':2,'05':2,'06':2,'07':3,'08':3,'09':3,'10':4,'11':4,'12':4})
df_day['date_id']= df_day.apply(lambda x:str(x['年'])+str(x['季度']),axis=1)

随后,需要将处理后的数据进行合并操作,我选择的连接方式是'outer',即按照每日的行情数据后面匹配季度更新的财务数据,最后得到的数据仍然是每日一行,只不过在每日行情后面增加了当天的财务数据。

df2 = pd.merge(df_day,df1_lrb,on='date_id',how='outer')
df2 = pd.merge(df2,df1_xjllb,on='date_id',how='outer')
df2 = pd.merge(df2,df1_zcfzb,on='date_id',how='outer')
df2 = pd.merge(df2,df1_cwzb,on='date_id',how='outer')
df2 = df2.loc[df2['年'].notnull()].sort_values(by = '日期').reset_index(drop=True)
df2 = df2.loc[df2['涨跌幅']!='None'].reset_index(drop=True)

该过程处理时间较长,因为是对每支个股自上市以来所有的交易日行情数据与当月的全部财务数据进行合并,但是这个过程是必要的,生成的总表一经存储,除非有数据更新的需要,以后的因子测试和回测都可以直接读取本地存储的总表进行测试。

到了这步,聪明的你应该已经能够想象到获得的总表数据大概长的啥样。没错,其实就是在日度行情数据横向添加了财务数据,然后把所有股票的表格进行了纵向合并。

wait a moment...

差点忘了说最关键的一步,需要对收盘价进行复权处理,具体处理方式不说了,直接上代码吧。如果看不懂可以私聊我。

shoupan = [df2['收盘价'][0]]
for i in range(1,len(df2['收盘价'])):
shou_fu = float(shoupan[-1])*(1+0.01*float(df2['涨跌幅'][i]))
shoupan.append(shou_fu)
df2['收盘价_复权'] = shoupan
df2 = df2.replace('--',0)
df2 = df2.replace(' --',0)
all_data = pd.concat([all_data,df2], ignore_index=True)

运行过程中出现部分股票报错,查看原因,发现是300186这支股票的行情数据为空,吓得我赶紧打开同花顺查了一下这个股票,原来这支名为大华农的股票在2015年已经退市了。由于之前是从东方财富网爬取的分行业股票代码,所以这个锅应该由东方财富背,已经退市的股票为什么还在行业股票池里呢?不过这对我们的分析没有影响,退市了的股票直接跳过就好,不会出现在最后的总表中。

将得到的数据命名为all_data并存到本地

all_data.to_csv('E:/医药制造/all_data.csv')

2、因子生成

直接读取前面存储到本地的all_data,

df = pd.read_csv('E:/医药制造/all_data.csv', low_memory=False)

查看总表df的列名,可以看到这个表一共有299列,26万多行。前几个列是日度行情数据,即量价数据、市值数据,后面的列分别是三张财务报表中的具体科目。

为什么有26万多行呢?其实是因为之前我爬取数据时候设置了爬取周期为2016年到2020年(见下图),所以得到的每支股票的交易日为200日*5年=1000个左右交易日,而医药制造行业除去已退市股票一共有260多支个股,所以260*1000=26万行。这说明上面的数据处理过程没有出错~

现在,我已经将回测期间所有股票的全部基础数据放到了总表all_data中,下面可以按照自己的需求构造一些衍生指标,比如量价指标、财务指标等等,我的基础指标有290多个,可以衍生出的指标不计其数,下面构造了几个简单的财务指标,

df['市盈率'] = df['收盘价']/df['基本每股收益(元)']
df['市净率'] = df['收盘价']/df['每股净资产(元)']
df['市销率'] = df['总市值']/df['主营业务收入(万元)']
df['市现率'] = df['收盘价']/df['每股经营活动产生的现金流量净额(元)']
df['ROA'] = df['净利润(万元)_y']/df['总资产(万元)']
df['存货周转率'] = df['主营业务收入(万元)']/df['存货(万元)']
df['总费用率'] = (df['营业税金及附加(万元)']+df['管理费用(万元)']+df['销售费用(万元)']+df['资产减值损失(万元)']+df['营业外收入(万元)']-df['营业外支出(万元)'])/df['营业收入(万元)']
df['资产杠杆'] = df['总资产(万元)']/(df['总资产(万元)']-df['总负债(万元)'])

3、因子测试

终于到了本文的主题,获得了这么多因子,怎么判断这些因子哪个能用呢?这里使用两种方法:回归法和IC值法。相信各位对这两种方法都不陌生,所以简单介绍。我将2016年-2019年作为训练集进行因子测试,2017年-2020年作为测试集进行回测。

回归法,即在2016-2019年共计48个月的截面期对第T期个股因子与第T+1期个股收益率进行回归,这样对于每个因子可以得到48个回归系数和48个相应的p值。

IC值法,同样,在2016-2019年共计48个月的截面期对第T期个股因子与第T+1期个股收益率计算相关系数,这样对于每个因子可以得到48个相关系数。

没错,我这里将上面得到的26万行的日度数据合并成了月度数据,并且进行按月因子回测和组合构建。

具体的,我按照经过复权的收盘价,定义函数get_month_return(df)得到个股在某个月的收益率。然后将T期因子值与T+1期收益率进行横向合并。上述过程在2016-2019年48个月的截面期上滚动进行。

def get_month_return(df):
return (df['收盘价_复权'].iloc[-1]-df['收盘价_复权'].iloc[0])/df['收盘价_复权'].iloc[0]
for year in range(2016,2020): #对于行情数据,可以每天滚动测试,此处简化处理。
for month in range(1,13):
params = []
pvalues = []
icvalue = []
df_this = df.loc[(df['年']==year) & (df['月']==month)]
gr_this = df_this.groupby('股票代码')
if month!=12:
df_next = df.loc[(df['年']==year) & (df['月']==month+1)]
gr_next = df_next.groupby('股票代码')
fac1 = gr_this['涨跌额'].std().reset_index()
fac2 = gr_this['换手率'].std().reset_index()
fac = pd.merge(fac1,fac2,on='股票代码',how='inner')
df_month = pd.merge(pd.merge(fac,gr_this.mean().reset_index(),on='股票代码',how='inner'),gr_next.apply(get_month_return).reset_index().rename({0:'收益率'},axis=1),on='股票代码',how='inner')
else:
df_next = df.loc[(df['年']==year+1) & (df['月']==1)]
gr_next = df_next.groupby('股票代码')
fac1 = gr_this['涨跌额'].std().reset_index()
fac2 = gr_this['换手率'].std().reset_index()
fac = pd.merge(fac1,fac2,on='股票代码',how='inner')
df_month = pd.merge(pd.merge(fac,gris.mean().reset_index(),on='股票代码',how='inner'),gr_next.apply(get_month_return).reset_index().rename({0:'收益率'},axis=1),on='股票代码',how='inner')

在对总表进行归一化和相应的填充缺_th失值处理后(过程略,感兴趣者私聊),即可进行各个因子在各个截面期上的滚动测试,

for factor in range(len(guiyi.columns)-1):
x = guiyi.iloc[:,factor]
y = guiyi.iloc[:,-1]
res = sm.OLS(y.astype(float), x.astype(float)).fit()
par = res.params.values[0]
pva = res.pvalues.values[0]
ic = y.corr(x)
params.append(par)
pvalues.append(pva)
icvalue.append(ic)
print(year,month)
result_params = result_params.append(pd.Series(params), ignore_index=True)
result_pvalue = result_pvalue.append(pd.Series(pvalues), ignore_index=True)
result_ic = result_ic.append(pd.Series(icvalue), ignore_index=True)

4、测试结果

最后,我对上面滚动测试的结果进行评价,即选择出同时通过回归法和IC值法测试的因子作为医药制造行业的有效因子。

先定义3个计数函数,分别计算48个截面期上各个因子的表现情况。其中count_p用来计算48个回归p值中小于10%的占比,从而判断因子与收益率相关关系是否显著;count_ic用来计算48个相关系数中IC值大于0的占比,从而判断因子变动方向与收益率变动方向是否一致;count_ic_percent用来计算48个相关系数中IC值大于0.03的占比,从而判断因子值与收益率是否具有较强的相关性。

def count_p(fac):
con = 0
for i in fac:
if i<0.1:
con+=1
percent = con/len(fac)
return percent

def count_ic(fac):
con = 0
for i in fac:
if i>0:
con+=1
percent = con/len(fac)
return percent

def count_ic_percent(fac):
con = 0
for i in fac:
if i>0.03:
con+=1
percent = con/len(fac)
return percent

然后构造最终的因子评价表,表格指标包括:'因子名称','p均值','p值小于10%比例','因子收益率均值','因子收益率p值','IC均值','IC标准差','IC值大于0比例','IC值大于0.03比例','IR值'等。综合这些指标,即可以选择出对于医药行业有效的因子。

result_pvalue.columns = guiyi.columns[:-1]
result_sum = pd.DataFrame() #['因子名称','p值小于10%比例','因子值均值']
for i in range(len(result_pvalue.columns)):
percent = count_p(result_pvalue.iloc[:,i]) #'p值小于10%比例'
pvmean = result_pvalue.iloc[:,i].mean() #'p均值'
pamean = result_params.iloc[:,i].mean() #'因子收益率均值'
pvalue = stats.ttest_1samp(result_params.iloc[:,i],0).pvalue #'因子收益率p值'
ic_mean = result_ic.iloc[:,i].mean() #'IC均值'
ic_std = result_ic.iloc[:,i].std() #'IC标准差'
percent1 = count_ic( result_ic.iloc[:,i]) #'IC值大于0比例'
percent2 = count_ic_percent( result_ic.iloc[:,i]) #'IC值大于0.03比例'
inf = [result_pvalue.columns[i],pvmean,percent,pamean,pvalue,ic_mean,ic_std,percent1,percent2]
result_sum = result_sum.append(pd.Series(inf), ignore_index=True)
print(result_pvalue.columns[i],pvmean,percent,pamean,pvalue,ic_mean,ic_std,percent1,percent2 )
result_sum.columns=['因子名称','p均值','p值小于10%比例','因子收益率均值','因子收益率p值','IC均值','IC标准差','IC值大于0比例','IC值大于0.03比例']
result_sum['IR'] = result_sum['IC均值']/result_sum['IC标准差']

在上图的表格中,按照IR值降序,即可看到各个因子的IR值排名。同理,对于其他指标也可以对全部因子进行排序。

下一篇进行回测,敬请关注。

如有以下需求请直接点击红字链接
数据资源
学术指导
数据众筹
Paper

Paper



资源获取方式

加客服1

更多资源,点击下方阅读原文

【声明】内容源于网络
0
0
Paper数据分析
资源分享、科研辅导、数据分析等干货基地
内容 136
粉丝 0
Paper数据分析 资源分享、科研辅导、数据分析等干货基地
总阅读49
粉丝0
内容136