基于百度指数的股票收益率多因子模型研究
时间:2023-06-12 02:07:00
研究基于百度指数的股票收益率多因子模型
- 导语
- 数据获取
-
- 基于qdata获取百度指数
- 基于efiance获取股票数据
- 数据集合并
- 获取运行数据
- 数据处理
- 可视化分析
-
- 百度指数之间的相关分析
- 热门话题时间及股票收益率总结
- 热门话题分布
- 相关话题的热度与股价和收益率的关系图
- 构建多因子模型
-
- CAPM
- 多因子策略
导语
个人投资者购买股票的决策过程如下所示
考虑到这样一个用户场景:在一个普通的交易日上午,个人投资者习惯性地①打开某财务管理APP(同花顺、东方财富、雪球股票等)检查可选股票的涨跌、股价、行业板块涨跌、个股龙虎榜等。如果你看到它,你几乎可以去信息部门查看头条新闻、文章推荐等。如果你感兴趣,你会②点击进去浏览,浏览上述信息后,看看某只股票③在搜索栏中寻找股票,点击查看相关具体信息(相关动态信息、开盘、研究报告等),然后可能在许多信息类别中APP(微博、百度、今日头条等)⑦搜索个股的相关话题和行业信息,最终可能④做出购买决定。
此时,整个决策过程的路径是①②③(⑦)④。
考虑另一个用户场景:忙碌了一天后,个人投资者回家刷快手抖音(信息娱乐)APP),APP上⑤推送财经博主的个股或公司解读视频,⑥他很感兴趣,所以他不知不觉地看完了视频。看了一些视频评论后,该看了APP或其他信息类APP中⑦搜索关于该个股的话题、相关行业的讯息,又或者会③打开理财类APP搜索股票或相关板块的具体信息,并考虑明天开盘买入。
此时,整个决策过程的路径是⑤⑥(③)(⑦)④。
可见,⑦在资讯类APP搜索这种行为从来都不是用户购买个股的唯一途径,但因为⑦主动搜索往往代表着用户对这类话题或相关股票的强烈兴趣。在当今的自媒体时代,种草经济和内容输出现象盛行,个人投资者在做出购买决策判断之前会想看别人的看法和看法,所以⑦这个职位对个人投资者来说仍然很重要。何况⑦在用户决策漏斗通往购买决策的最后一关,理论上转化率会更高,所以如果能量化的话⑦的一个搜索量,也算约等于知道有多少用户处于④在购买决策中。
而根据流动性溢价原则上,当越来越多的人关注或想要获得一只股票时,即股票的流动性增加甚至扩大,股价将在一定程度上超过其应有的价值,导致错误的定价,即溢价。此时,该股票的短期内将产生超额回报率。当然,股票的长期回报率仍将接近其真实价值,毕竟,市场是短期投票器,长期是称重器。
幸运的是,有一种方法可以量化⑦搜索量是通过百度指数表示的⑦定量指标,然后探索相关主题搜索量与个股短期收益率的关系,验证上述讨论,预计将相关主题的搜索量添加到定量投资多因素模型中,增加定量投资的收益。
#导入第三方包 from qdata.baidu_index import get_search_index from qdata.baidu_index.common import split_keywords from datetime import datetime, timedelta import time import pandas as pd from datetime import datetime, timedelta import efinance as ef
数据获取
基于qdata获取百度指数
def baidu_index(keywords_list,cookies,start_date,day_diff): #获取百度指数第三方包原址:https://github.com/longxiaofei/spider-BaiduIndex df_index=pd.DataFrame() keywords_list = keywords_list cookies = cookies start_date= start_date day_diff=day_diff #输入统一日期格式,以便后续获取相同时间段的股票数据 start_date_tuple=datetime.strptime(start_date,'%Y-%m-%d') end_date_tuple=start_date_tuple timedelta(days=day_diff) end_date=end_date_tuple.strftime('%Y-%m-%d')/span> i=0 #控制只保留一列日期数据 date_list=[] for keywords in split_keywords(keywords_list): for single_keyword in keywords: print(single_keyword) time.sleep(15) index_list=[] try: for index in get_search_index( keywords_list=[single_keyword], start_date=start_date, end_date=end_date, cookies=cookies ): index_list.append(index['index']) date_list.append(index['date']) i+=1 if i ==1: df_index['date']=date_list df_index[index['keyword'][0]]=index_list except Exception as e: pass df_index=df_index.head(int(len(df_index)/3)) #亲测qdata获取的百度指数在300条以内的时候是会重复提取3次的,且大于300条时数据会混乱 return df_index
基于efiance获取股票数据
def stock_data(stock_code,start_date,day_diff):
#获取股票数据第三方包原地址:https://github.com/Micro-sheep/efinance
stock_code = stock_code
start_date= start_date
day_diff=day_diff
start_date_tuple=datetime.strptime(start_date,'%Y-%m-%d')
end_date_tuple=start_date_tuple+timedelta(days=day_diff)
start_day= start_date_tuple.strftime('%Y%m%d')
end_day= end_date_tuple.strftime('%Y%m%d')
df_stock = ef.stock.get_quote_history(stock_code, beg=start_day, end=end_day)
df_stock=df_stock[['日期','收盘']]
df_stock.columns=['date','close']
df_sh = ef.stock.get_quote_history('上证指数', beg=start_day, end=end_day)
df_sh=df_sh[['日期','收盘']]
df_sh.columns=['date','sh']
df_stock=pd.merge(df_stock,df_sh)
return df_stock
数据集合并
def initial_data(df_stock,df_index):
#合并百度指数和股票指数的数据集
df_backup=pd.merge(df_stock,df_index,on='date',how='right')
df_backup.fillna(method='ffill',inplace=True)
df_backup['date']=pd.to_datetime(df_backup['date'],format='%Y-%m-%d')
for i in range(3,len(df_backup.columns)):
df_backup.iloc[:,i]=df_backup.iloc[:,i].astype('int')
return df_backup
运行数据获取
keywords_list=[['中兴通讯'],['zte'],['5G'],['边缘计算'],['物联网'],['新能源汽车'],['自动驾驶'],['车联网'],['ICT'],['数字经济'],['通讯行业'],['智慧城市'],['人工智能'],['网络安全']]
cookies = """BIDUPSID=187E54B46AD95D7D0C6CA7C17A441A00; PSTM=1652806892; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598; BAIDUID=8769DC8382346675420D39751AB8C7F4:FG=1; Hm_lvt_d101ea4d2a5c67dab98251f0b5de24dc=1654905894; delPer=0; PSINO=6; ZFY=MwsVhdDRPk111Nng:AhOi4P1nZQRrR1Mc6azfnAjjqKo:C; BAIDUID_BFESS=8769DC8382346675420D39751AB8C7F4:FG=1; BDRCVFR[feWj1Vr5u3D]=I67x6TjHwwYf0; BA_HECTOR=210k2ha4252ha00h8g1hahv6d15; H_PS_PSSID=36426_36559_36624_36592_36455_31253_36511_36452_36420_36166_36520_26350_36469_36314; BCLID=7189016615800227754; BDSFRCVID=iNPOJexroG0leprDmD978gjVjopWxY5TDYrELPfiaimDVu-VJeC6EG0Pts1-dEu-EHtdogKK3gOTH4AF_2uxOjjg8UtVJeC6EG0Ptf8g0M5; H_BDCLCKID_SF=tR30WJbHMTrDHJTg5DTjhPrMLN3dWMT-MTryKKJs54JKshOnBn7b-q4vXp5jLbvkJGnRh4oNBUJtjJjYhfO45DuZyxomtfQxtNRJQKDE5p5hKq5S5-OobUPUjfc9LUkqW2cdot5yBbc8eIna5hjkbfJBQttjQn3hfIkj2CKLtCvEDRbN2KTD-tFO5eT22-usMeQR2hcHMPoosIJ1bJ3KQ5K8b4vf5lRRB5rj--nwJxbUotoHXh3tMt_thtOp-CrpWDTm_q5TtUJMqIDzbMohqfLn5MOyKMniJCj9-pPKWhQrh459XP68bTkA5bjZKxtq3mkjbPbDfn028DKuDjREh40822Ta54cbb4o2WbCQfnkV8pcN2b5oQT8jbq3H0T57JGQf3x7gB-o2SPnXjqOUWJDkXb3ha4o0amnpL66CWJ5TMl5jDh3MKToDb-otexQ7bIny0hvcJR6cShPCyUjrDRLbXU6BK5vPbNcZ0l8K3l02V-bIe-t2XjQhDHR02t3-MPoa3RTeb6rjDnCry-5UXUI82h5y05OQ56RHKIQh556bHf7eXn523nk4jJORXRj4BNRhBRjValb4fj6Ky4oTjxL1Db3Jb5_L5gTtsl5dbnboepvojtcc3MvByPjdJJQOBKQB0KnGbUQkeq8CQft20b0EeMtjW6LEtR30WJbHMTrDHJTg5DTjhPrMMtTTWMT-MTryKKJs54JKsb6eejtWbPIZMxnjLbvkJGnRh4oNBUJtjJjYhfO45DuZyxomtfQxtNRJQKDE5p5hKq5S5-OobUPUDUJ9LUkJ0mcdot5yBbc8eIna5hjkbfJBQttjQn3hfIkj2CKLK-oj-D8Ge5u53e; BCLID_BFESS=7189016615800227754; BDSFRCVID_BFESS=iNPOJexroG0leprDmD978gjVjopWxY5TDYrELPfiaimDVu-VJeC6EG0Pts1-dEu-EHtdogKK3gOTH4AF_2uxOjjg8UtVJeC6EG0Ptf8g0M5; H_BDCLCKID_SF_BFESS=tR30WJbHMTrDHJTg5DTjhPrMLN3dWMT-MTryKKJs54JKshOnBn7b-q4vXp5jLbvkJGnRh4oNBUJtjJjYhfO45DuZyxomtfQxtNRJQKDE5p5hKq5S5-OobUPUjfc9LUkqW2cdot5yBbc8eIna5hjkbfJBQttjQn3hfIkj2CKLtCvEDRbN2KTD-tFO5eT22-usMeQR2hcHMPoosIJ1bJ3KQ5K8b4vf5lRRB5rj--nwJxbUotoHXh3tMt_thtOp-CrpWDTm_q5TtUJMqIDzbMohqfLn5MOyKMniJCj9-pPKWhQrh459XP68bTkA5bjZKxtq3mkjbPbDfn028DKuDjREh40822Ta54cbb4o2WbCQfnkV8pcN2b5oQT8jbq3H0T57JGQf3x7gB-o2SPnXjqOUWJDkXb3ha4o0amnpL66CWJ5TMl5jDh3MKToDb-otexQ7bIny0hvcJR6cShPCyUjrDRLbXU6BK5vPbNcZ0l8K3l02V-bIe-t2XjQhDHR02t3-MPoa3RTeb6rjDnCry-5UXUI82h5y05OQ56RHKIQh556bHf7eXn523nk4jJORXRj4BNRhBRjValb4fj6Ky4oTjxL1Db3Jb5_L5gTtsl5dbnboepvojtcc3MvByPjdJJQOBKQB0KnGbUQkeq8CQft20b0EeMtjW6LEtR30WJbHMTrDHJTg5DTjhPrMMtTTWMT-MTryKKJs54JKsb6eejtWbPIZMxnjLbvkJGnRh4oNBUJtjJjYhfO45DuZyxomtfQxtNRJQKDE5p5hKq5S5-OobUPUDUJ9LUkJ0mcdot5yBbc8eIna5hjkbfJBQttjQn3hfIkj2CKLK-oj-D8Ge5u53e; BDUSS=3Q0fndZYmhwTUx1clRIakN3MGFQZ1dvdVFoTVRYYXZodjd5aGNTY3QxWXdqdEJpSVFBQUFBJCQAAAAAAAAAAAEAAACFwjdLanVubmp1bm5iaGFwcHkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADABqWIwAalia; SIGNIN_UC=70a2711cf1d3d9b1a82d2f87d633bd8a04052555077nxoxWNVqgLJ6Xa0qo%2F6rmA4PAvmTQkcjbUJg35LkCEWutkQ74GGWD038uzCXIfqCas7eHeIAN%2Bv%2BPVN55Wx8xvWfWFiBOEYRPMOwl2abJVMRF4lMo2oHx8ECV4qxoJ93XMBJGtQgEdvUET6q5H0buMrQaULeAQp%2Bi2oLaW2E8gr3boMVotEfJngpS98GYj8ICGLjCD0NYN%2FVOrtGOeaTXpNqMdMUX1AODWyflIby6TyoMXhFGZ82ofRnZgpXpl05YLqOoCgJXa4yYhzwzknhZGMhtWbqPQPNlUdAbZQwpm4%3D96416976362718933879039618364210; __cas__rn__=405255507; __cas__st__212=f7adc00a94a71e45793164597a0f3e17f64a496e22409f08bb0a3d40204c45fa694168b8a1de0d302a1a387f; __cas__id__212=40859543; CPID_212=40859543; CPTK_212=30581114; Hm_up_d101ea4d2a5c67dab98251f0b5de24dc=%7B%22uid_%22%3A%7B%22value%22%3A%221261945477%22%2C%22scope%22%3A1%7D%7D; bdindexid=kpp4j4d1kq6vuhqu55f1n3dkl5; Hm_lpvt_d101ea4d2a5c67dab98251f0b5de24dc=1655243068; ab_sr=1.0.1_ZThlZmY5MWJjZDcyMDk0ZGY4NTY5OTM5ZGNkMjkzOWY3MDZkYzE0NTY0MWIwYWRjZDFhMjAwMzM5ZTA3MzlhOTA5MjAxMjc5M2ZmMDIzNTUyOTNmZWY1MzRjMjIxMzJlMWM5ZGYzN2YxMmMxNmRhNTQ1MTEwMDEwNTc2YzA2ZGEyYTBjN2JkYjcwZjg1MmFhMGRjMDU4NjYxMDMwMTQ3NQ==; BDUSS_BFESS=3Q0fndZYmhwTUx1clRIakN3MGFQZ1dvdVFoTVRYYXZodjd5aGNTY3QxWXdqdEJpSVFBQUFBJCQAAAAAAAAAAAEAAACFwjdLanVubmp1bm5iaGFwcHkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADABqWIwAalia; RT="z=1&dm=baidu.com&si=j93mjd353ci&ss=l4eowfjs&sl=f&tt=10hw&bcn=https%3A%2F%2Ffclog.baidu.com%2Flog%2Fweirwood%3Ftype%3Dperf&ld=34nb&ul=3ppr"""
start_date= '2020-01-02'
day_diff=300 #获取百度指数的总天数。注意:亲测qdata获取的百度指数在300条以内的时候是会重复提取3次的,且大于300条时数据较乱,所以尽量控制day_diff<=300
stock='中兴通讯'
Rf_year=0.04 #无风险年化利率
diff_num=3 #几日收益率
threshold=1.2
df_index=baidu_index(keywords_list,cookies,start_date,day_diff)
df_stock=stock_data(stock,start_date,day_diff)
df_backup=initial_data(df_stock,df_index)
df_backup
数据处理
百度指数的官方定义是:以网民在百度的搜索量为数据基础,以关键词为统计对象,科学分析并计算出各个关键词在百度网页搜索中搜索频次的加权。
所以每个关键词之间都可能有数量级的差异,这会一定程度上夸大数量级大的关键词的作用且忽略数量级小的关键词。在实际场景中,某一关键词的百度指数的变化或者说异常高的点比它的绝对值更具有参考价值,因为互联网用户对关键词搜索关注程度的突发性爆发会更能代表某一股票的流动性爆发从而造成市场的错误定价。所以要先以百度指数为基础新设立一个新的指标–话题指数change_index来量化当天的一个关键词的百度指数偏离了多少该关键词平均的百度指数。
1.先用pairplot函数查看百度指数的数值分布。
sns.pairplot(df_backup)
2.如图上所示,百度指数的数值分布近似于正态分布,这时异常值检测最基础的方法就是标记出与平均值偏差k倍sigma(标准差)的数值,但由于这种异常值检测是相对于一整个时间段而言的,检测出来的异常值始终是大于某一特定数值,这时互联网用户对关键词的爆发就相对于一个较长的时间段而言了。而一种更进阶的做法是,设定一个时长为5天的时间窗口,标记出与前5天平均值偏差k倍sigma的数值作为异常值,就能不仅能标记出绝对峰值,也能标记出百度指数的绝对峰值了。
#探究移动sigma与整体sigma方法标记异常值(被关注的话题)的效果对比
topic=df_comb.columns[4]
sigma=(df_comb[topic]-df_comb[topic].mean())/df_comb[topic].std()
df_rolling_method=df_comb[df_comb['change_'+topic]>=threshold]
df_total_method=df_comb[sigma>=threshold]
fig, (ax1,ax2)=plt.subplots(2,sharex=True)
ax1.plot(df_comb['date'],df_comb[topic],linestyle='--',color='tab:purple')
ax1.plot(df_rolling_method['date'],df_rolling_method[topic],'ro',markersize=2)
ax1.set_ylabel('rolling method')
ax2.plot(df_comb['date'],df_comb[topic],linestyle='--',color='tab:purple')
ax2.plot(df_total_method['date'],df_total_method[topic],'ro',markersize=2)
ax2.set_ylabel('total method')
#优化日期格式
locator = mdates.AutoDateLocator(minticks=10, maxticks=15)
formatter = mdates.ConciseDateFormatter(locator)
ax1.xaxis.set_major_locator(locator)
ax1.xaxis.set_major_formatter(formatter)
ax1.xaxis.set_minor_locator(mdates.MonthLocator())
fig.savefig('image/相对峰值与绝对峰值.jpg',dpi=500)
3.将话题指数change_index(k)
4.分别计算上证指数和选定股票的短期收益率
def stock_topic(df):
for col in range(3,len(df.columns)):
single_col = df.iloc[:,col]
moving_avg = single_col.rolling(window=5).mean(center=True) #计算各话题指数的5日移动平均值
moving_std = single_col.rolling(window=5).std(center=True) ##计算各话题指数的5日移动标准差值
col_name='initial_change_'+df.columns[col]
df[col_name]=(single_col-moving_avg)/moving_std
col2_name='change_'+df.columns[col]
df[col2_name]=df.apply(lambda x:x[col_name] if x[col_name]>=threshold else 0 ,axis=1) #将超过平均值的threshold倍标准差的数值保留,并将没超过的设为0
df.fillna(0,inplace=True)
df_comb=df.reset_index(drop=True)
Rf=(1+Rf_year)**(1/365)-1 #计算无风险日利率
df_comb['sh_ret']=(df_comb['sh']/(df_comb['sh']-df_comb['sh'].diff(diff_num))-1-Rf)*100 #计算上证指数的diff_num日收益率百分比数
df_comb['close_ret']=(df_comb['close']/(df_comb['close']-df_comb['close'].diff(diff_num))-1-Rf)*100 #计算要研究的股票的diff_num日收益率百分比数
df_comb.dropna(inplace=True)
df_comb.reset_index(drop=True,inplace=True)
return df_comb
pd.options.display.encoding = 'GBK'
df=df_backup.copy()
df_comb=stock_topic(df)
df_comb
可视化分析
各百度指数间相关性分析
df=df_backup.copy()
df_corr = df.corr(method='pearson')
cmap = sns.diverging_palette(220, 10, as_cmap=True)
fig, ax = plt.subplots(figsize=(10,10))
pd.options.display.encoding = 'GBK'
sns.heatmap(df_corr, cmap=cmap, vmax=1.0, center=0, fmt='.2f',
square=True, linewidths=.5, annot=True, cbar_kws={
"shrink": .75})
ax.set_title('各特征的皮尔逊系数')
plt.savefig('image/各特征的皮尔逊系数.jpg',dpi=300)
plt.show()
皮尔逊系数-Pearson代表着两两特征的相关系数,趋近于-1表示线性负相关,趋近于0表示线性不相关,趋近于1表示线性正相关。如图上所示,大部分特征相关性较低,表示后面训练出来的多因子模型不太会受到多重共线性的影响。
话题热度时间及股票收益率总览
import matplotlib.pyplot as plt
import seaborn as sns
x_columns_list=[]
x_columns_list.append('date')
for i in range(3,len(df_backup.columns)):
x_columns_list.append('change_'+df_backup.columns[i])
df_map=df_comb[x_columns_list]
df_map.set_index('date',inplace=True)
df_map.T
stock='sh'
df_hot=df_comb[df_comb['change_'+topic]>=threshold]
cmap = sns.diverging_palette(220, 10, as_cmap=True)
fig, (ax1,ax2) = plt.subplots(2,figsize=(10,10))
sns.heatmap(df_map.T, cmap=cmap, vmax=1.0, center=0, fmt='.2f',ax=ax1,cbar=False,
square=False, linewidths=.5, annot=False, cbar_kws={
"shrink": .75},xticklabels=False,yticklabels=True)
ax1.set_ylabel('话题热度时间表')
ax2.plot(df_comb['date'],df_comb['close_ret'],label=stock+'_stock')
ax2.set_ylabel('股票收益率')
plt.savefig('image/话题热度时间及股票收益率总览.jpg',dpi=300)
plt.show()
话题热度分布
df_index_change=df_comb[x_columns_list]
df_index_change=df_index_change.iloc[:,1:]
empty_num_list=[]
specific_num_list=[]
for col in df_index_change.columns:
empty_num_list.append(sum(df_index_change[col]==0))
specific_num_list.append(sum(df_index_change[col]!=0))
N = len(empty_num_list)
ind = np.arange(N) # the x locations for the groups
width = 0.35 # the width of the bars: can also be len(x) sequence
fig, ax = plt.subplots()
p1 = ax.bar(ind, empty_num_list, width, label='话题未被关注的天数')
p2 = ax.bar(ind, specific_num_list, width,bottom=empty_num_list,label='话题被关注的天数')
ax.axhline(0, color='grey', linewidth=0.8)
ax.set_title('话题热度分布')
ax.set_xticks(ind, labels=df_index_change.columns)
ax.legend()
ax.bar_label(p1, label_type='center')
ax.bar_label(p2, label_type='center')
plt.setp(ax.get_xticklabels(), rotation=90, horizontalalignment='right')
plt.legend(loc='lower left')
plt.savefig('image/话题热度分布.jpg',dpi=300)
plt.show()
在样本量为300天的数据集中,各话题百度指数爆发的天数大约为总样本的10%-20%, 存在真实样本数不足的隐患,后续可以增加样本量的提取以解决此问题。
相关话题热度与股票股价及收益率的关系图
for i in range(3,len(df_backup.columns)): topic=df_backup.columns[i] df_hot=df_comb[df_comb['change_'+topic]>threshold] fig, (ax1,ax2,ax3)=plt.subplots(3,sharex=True) ax1.plot(df_comb['date'],df_comb[topic],linestyle='--',color='tab:purple',label=topic) ax1.plot(df_hot['date'],df_hot[topic],'ro',markersize=2) ax1.set_ylabel(topic+'_index') ax2.plot(df_comb[元器件数据手册
、IC替代型号,打造电子元器件IC百科大全!