研报复现系列(一):【方正证券】跟踪聪明钱:从分钟行情数据到选股因子

1.研报概述

本文是研报复现系列的第一篇,文本复现了【方正证券】的研报【跟踪聪明钱:从分钟行情数据到选股因子】。

该研报尝试从分钟行情数据中挖掘出那些聪明人(即机构)所做的交易,称为“聪明钱”,并量化这些聪明钱对后市看涨、看跌的情绪观点,进而跟随聪明钱进行选股。

2.研究环境

JointQuant

import datetime
import math
import matplotlib.pyplot as plt
from jqdata import *
import pandas as pd
import numpy as np
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False 

3.研报复现

3.1 数据获取

本文数据来源于聚宽JoinQuant。用到的接口名和功能如下表所示。具体参数请查阅聚宽的API文档。

接口名 功能
get_bars 获取行情数据
get_price 获取获取数据、是否停牌、涨跌停价
get_trade_days 获取一段时间内的交易日列表
get_extras 查询特定股票是否是ST股
get_all_securities 获取股票数据,包括上市时间
get_index_stocks 获取指数成分股

3.2 划分聪明钱

聪明钱在交易过程中往往呈现“单笔订单数量更大、订单报价更为激进”

因此,某分钟的交易的聪明度指标S定义为:S= ∣ R t ∣ \left| Rt \right| Rt / / / V t \sqrt{Vt} Vt

Rt为第t分钟的涨跌幅,Vt为第t分钟的成交量。S的值越大,则说明此交易越聪明。

我们封装一个获取分钟行情数据以及S值的函数,方便后面的研究使用。

def get_minute_data(
    security,#股票代码,可以是list或者字符串
    count,#获取多少条数据
    end_dt=datetime.datetime.now()
):
df = get_bars(
        security,
count = count,
include_now=True,
unit = '1m',
fields = ['date','open','close','volume'],
df = True,
end_dt = end_dt
    )
    
df['R'] = (df['close'] - df['open'])/df['open']#涨跌幅
df['S'] = abs(df['R'])/df['volume']**0.5*10000 #S值,乘10000为了方便画图
df['volume'] = df['volume']/10000#volume除以10000方便画图
return df

原研报利用成交量与计算出的S值来划分聪明钱交易。

划分算法为:
step1:对于特定股票,特定时间段的分钟级交易数据,计算得到每分钟的S值,并将数据按照S值由大到小排序。
step2:求排序后的分钟数据的交易量累加和,将交易量累加和占该该时间段总成交量前20%的分钟交易视为由聪明钱进行的交易。

其中,阈值选取20%是因为在A股市场中机构交易者只贡献了少量的交易量,原研报同时测试了阈值选取其他数值时效果,其中20%是最优的.

我们以603198.XSHG在2021年4月8日下午14:00到14:30的分钟数据为例,给出划分聪明钱的程序并将结果可视化。

'''
功能:
    寻找聪明钱的交易并可视化
算法:
     对于特定的股票,特定时间段的分钟级交易数据,计算得到每分钟的S值,并将数据按照S值由大到小排序。
     求排序后的分钟数据的交易量累加,将交易量累加和占比前20%的分钟视为由聪明钱交易的时刻。
     其中,阈值选取20%是因为在A股市场中机构交易者只贡献了少量的交易量,原研报同时测试了阈值选取其他数值时效果,其中20%是最优的。
数据:
     以603198.XSHG在2021年4月8日下午14:00到14:30的分钟数据为例。
'''
#获取数据并且个每个分钟编号
df = get_minute_data('603198.XSHG',30,end_dt='2021-04-08 14:30:00')
df['bar_no'] = [_ for _ in range(1,31)]#每根bar的序号,方便x轴的显示

#绘图
fig = plt.figure(figsize=(14,10))
#S值与成交量图
ax = fig.add_subplot(211)
h1 = ax.bar(df.index,df['volume'],label='成交量',width=0.5)#成交量
ax2 = ax.twinx()
h2 = ax2.scatter(df.index, df['S'],label='S因子',marker='o',color='r')#S值
plt.legend([h1,h2], [h1.get_label(),h2.get_label()], loc='best')
ax.grid()#网格
ax.set_ylabel(u"成交量(万)")
ax.set_title(u"成交量与S因子值")
ax2.set_ylabel(u"S值(10e-4)")
ax.set_xlabel(u"时间序号")
plt.xticks(df.index.values, df['bar_no'])

#排序后的图
ax = fig.add_subplot(212)
df = df.sort_values(by='S',ascending=False).reset_index(drop=True)#按S值排序
df['acc_volume_pct'] = df['volume'].cumsum()/df['volume'].sum()#计算累计成交量
df1 = df[df['acc_volume_pct']<=0.2]#累计成交量前20%
df2 = df[df['acc_volume_pct']>0.2]
ax.bar(df1.index,df1['volume'],color='r',width=0.5)#绘制累计成交量前20%的柱状图
h1 = ax.bar(df2.index,df2['volume'],label='成交量',width=0.5)#绘制余下的
ax2 = ax.twinx()
h2 = ax2.plot(df.index, df['acc_volume_pct'],label='累计成交量占比',marker='o',color='g')#累计成交量
plt.legend([h1,h2[0]], [h1.get_label(),h2[0].get_label()], loc='best')
ax.grid()
ax.set_ylabel(u"成交量(万)")
ax.set_title(u"选择聪明钱")
ax2.set_ylabel(u"累计成交量")
ax.set_xlabel(u"时间序号")
plt.xticks(df.index.values, df['bar_no'])

图片

3.3 量化聪明钱对股票后市的情绪

我们划分出了聪明钱的交易后,如何判断聪明钱是对这只股票看多还是看空呢

对于特定股票、特定时段的分钟行情数据,按照上述方法划分出聪明钱的交易之后,我们可以构造聪明钱的情绪因子Q

Q = vwap_smart/vwap_all

vwap_smart和vwap_all分别是聪明钱的成交量加权平均价和该段时间内所有分钟的成交量加权平均价

因子 Q 实际上反映了在该时间段中聪明钱参与交易的相对价位。之所以将其称为聪明钱的情绪因子,是因为因子 Q 的值越大,表明聪明钱的交易越倾向于出现在价格较高处,这是逢高出货的表现,反映了聪明钱的看空,消极情绪;因子Q的值越小,则表明聪明钱的交易多出现在价格较低处,这是逢低吸筹的表现,看多,积极情绪。

我们将情绪因子Q的计算封装成一个函数

def calcQ(data_bar):
    '''
    功能:
        计算Q值
    参数:
        DataFrame类型,特定股票一定时间段内的分钟数据(close,volume和S)
    返回:
        浮点数,即Q值
    '''
    data_bar = data_bar.sort_values(by='S',ascending=False).reset_index(drop=True)#按S值排序
    data_bar['acc_volume_pct'] = data_bar['volume'].cumsum()/data_bar['volume'].sum()#计算累计成交量
    smart_bar = data_bar[data_bar['acc_volume_pct']<=0.2]#聪明钱交易,累计成交量前20%
    #vwmap:volume weighted average price,成交量加权平均价
    vwap_smart = (smart_bar['close']*smart_bar['volume']).sum()/smart_bar['volume'].sum()
    vwap_all = (data_bar['close']*data_bar['volume']).sum()/data_bar['volume'].sum()
    return vwap_smart/vwap_all

3.4因子选股能力验证

本小节对上一小节量化出的聪明钱情绪因子的选股能力进行验证。

样本空间:沪深300成分股,剔除 ST 股和上市未满 60 日的新股,以及涨停、停牌、跌停(原研报为全部A股,但数据量过多,受硬件限制本文缩小了样本范围,做了简化)

测试时间:2013-04-30 到 2016-05-31

测试方法:每个月末时,对于选股空间的所有股票,计算其之前10天的分钟级交易数据的Q值,和该股票次月的收益率。这样每个月可以得到两组序列,分别为每只股票的Q值和次月的收益率,计算其RankIC秩相关系数。可视化测试时间周期内所有月份的两组序列的相关系数,统计显著相关情况。

概念解释

RankIC秩相关系数为两组序列中的元素在序列中的相对位置(即大小顺序)的相关系数。

在因子模型中,因子与收益的相关系数大于0.03即认为是有效的因子。

为了实现对因子的验证,我们封装了两个简单的函数,分别用于

1.获取特定日期满足交易条件的股票(即在选股样本中剔除ST股、涨停、跌停等)

2.获取特定日期所在月的最后一日,用于获取次月的行情数据

def get_stocks(date,index=None):
    '''
    功能:
        根据日期,获取该日满足交易要求的股票相关数据,即剔除ST股、上市未满60天、停牌、跌涨停股
    参数:
        date,日期
        index,指数代码,在特定指数的成分股中选股。缺省时选股空间为全部A股
    返回:
        DataFrame类型,索引为股票代码,同时包含了价格数据,方便后续使用
    '''
    stocks = get_all_securities(
        types=['stock'],
        date=date
    )#该日正在上市的股票
    
    if index:#特定成分股
        stock_codes = get_index_stocks(index,date=date)#成分股
        stocks = stocks[stocks.index.isin(stock_codes)]
        
    #上市日期大于60个自然日
    #date = datetime.datetime.strptime(date,'%Y-%m-%d').date()
    stocks['datedelta'] = date - stocks['start_date']
    stocks = stocks[stocks['datedelta'] > datetime.timedelta(days=60)]
    
    #是否是ST股
    stocks['is_st'] = get_extras(
        info='is_st',
        security_list=list(stocks.index),
        count=1, 
        end_date=date
    ).T
    
    #涨停、跌停、停牌
    stocks_info = get_price(
        security = list(stocks.index),
        fields=['close','high','low','high_limit','low_limit','paused'],
        count=1,
        end_date=date,
        panel=False
    ).set_index('code').drop('time',axis=1)
    
    stocks['price'] = stocks_info['close']#顺便保存价格,方便后续运算
    stocks['paused'] = stocks_info['paused'] == 1#是否停牌
    stocks['high_stop'] = stocks_info['high'] >= stocks_info['high_limit']#涨停
    stocks['low_stop'] = stocks_info['low'] <= stocks_info['low_limit']#跌停
    stocks = stocks[~(stocks['is_st'] | stocks['paused'] | stocks['high_stop'] | stocks['low_stop'])]   
    return stocks
def last_day_of_month(any_day):
    #获取某个日期所在月份的最后一天,方便在计算次月收益时获取次月的时间范围
    next_month = any_day.replace(day=28) + datetime.timedelta(days=4)  # this will never fail
    return next_month - datetime.timedelta(days=next_month.day)

万事俱备,我们现在统计每个月份每只股票的Q值和次月收益率,并计算秩相关系数、可视化。

#获取交易日序列
trade_days = get_trade_days(start_date='2013-04-30', end_date='2016-05-31')
#存储结果
result={
    'months':[],#月份序列
    'stocks':[],#股票序列
    'Q':[],#Q值序列
    'return':[]#次月收益序列
}
days_count = len(trade_days)
for i,date in enumerate(trade_days):#遍历交易日
    if i == days_count-1:#如果是列表最后一个日期
        #可交易的股票
        stocks = get_stocks(trade_days[i],index='000300.XSHG')
        stocks_list = list(stocks.index)
        
        #每只股票下个月的价格
        next_month_bar = get_bars(
            stocks_list, 
            count = 1, 
            unit = '1M',
            fields = ['open','close'],
            df = True,
            include_now=True,
            end_dt = '2016-06-30'
        )
              
        '''
        获取分钟数据,用来计算Q
        
        这里一次性获取所有股票的数据,来减少数据请求的次数,加速程序运行
        '''
        data = get_minute_data(
            stocks_list,
            2400,
            end_dt=trade_days[i]
        ).reset_index(level=0). rename(columns={'level_0':'code'})
        
        data_groups = data.groupby('code')#分钟数据根据股票代码分组
        
        result['stocks']+=stocks_list
        result['months']+=[trade_days[i].strftime('%Y-%m')]*stocks.shape[0]
        #依次计算每一只股票的Q值
        result['Q']+=[calcQ(data_bar) for name,data_bar in data_groups]
        result['return']+=list((next_month_bar['close']-next_month_bar['open'])/next_month_bar['open'])       
        break        
    if trade_days[i+1].month != trade_days[i].month:#每月的最后一个交易日
        stocks = get_stocks(trade_days[i],index='000300.XSHG')
        stocks_list = list(stocks.index)
        
        next_month_bar = get_bars(
            stocks_list, 
            count = 1, 
            unit = '1M',
            fields = ['open','close'],
            df = True,
            include_now = True,
            end_dt = last_day_of_month(trade_days[i+1])
        )
        
        data = get_minute_data(
            stocks_list,
            2400,
            end_dt=date
        ).reset_index(level=0).rename(columns={'level_0':'code'})
        data_groups = data.groupby('code')
        
        result['stocks']+=stocks_list
        result['months']+=[date.strftime('%Y-%m')]*stocks.shape[0]
        result['Q']+=[calcQ(data_bar) for name,data_bar in data_groups]
        result['return']+=list((next_month_bar['close']-next_month_bar['open'])/next_month_bar['open'])

df = pd.DataFrame(result)
'''
因为该段程序需要运行的时间较长,所以将运行出的结果存储到文件中
之后的研究可以直接读取文件
'''
df.to_excel('result.xlsx')
result = pd.read_excel('result.xlsx').drop('stocks',axis=1)
result_group = result.groupby('months')
#存储月份和每月两个序列的秩相关系数
result_df = {
    'month':[],
    'RankIC':[]
}
for month,month_result in result_group:
    result_df['month'].append(month)
    result_df['RankIC'].append(month_result.corr(method='spearman')['Q']['return'])
#以0.03作为阈值来划分显著和不显著相关    
result_df = pd.DataFrame(result_df)
ic_plus = result_df[result_df['RankIC']>0.03]
ic_minus = result_df[result_df['RankIC']<-0.03]
ic_other = result_df[(result_df['RankIC']>=-0.03) &(result_df['RankIC']<=0.03)]
print('显著为负: ',ic_minus.shape[0])
print('显著为正: ',ic_plus.shape[0])
print('不显著:',ic_other.shape[0])

#可视化
fig = plt.figure(figsize=(14,5))
ax1 = fig.add_subplot(111)
ax1.bar(ic_other.index, ic_other['RankIC'], align='center', width=0.5, color='pink')
ax1.bar(ic_plus.index, ic_plus['RankIC'], align='center', width=0.5, color='r')
ax1.bar(ic_minus.index, ic_minus['RankIC'], align='center', width=0.5, color='b')
ax1.set_ylabel(u"Rank IC")
ax1.set_title(u"因子 RankIC 的月度序列")
ax1.set_xlabel(u"时间" )
ax1.grid(axis='y')
plt.xticks(result_df.index, result_df['month'],ro

显著为负: 22

显著为正: 7

不显著: 8

图片

由结果来看,股票Q值与次月收益率在大部分时间显著为负,这也许上文在定义Q时的分析一致。Q值越大,聪明钱对该只股票看跌,聪明钱在高位抛售该股,该股次月的收益率下降。

3.5 选股策略的构建与回测

研究方法:每月月底将股票前10日分数数据的Q值排序、根据Q值由小到大等分为5类。每一类分别建仓,可视化每一类的收益情况。

研究结果:在策略运行的时间周期内,5类持仓的收益率具有单调性。即Q值小的组收益率大。这也可以由之前的分析解释,因为Q值小意味着聪明钱在低价吸入,聪明钱具有看多的情绪。

策略构建:基于研究结果,可构建两种策略。

一种是多空对冲策略,即做多第一组的股票。做空第五组的股票,进行对冲。该对冲策略在本文的回测中的收益率高于五组分别做多的收益,且最大回撤更低。在原研报的所有A股的样本中,对冲策略的收益率位于第一组和第三组之间,且最大回撤更低。

另一种是做多第一组的股票,但是要剔除前期涨幅过高的五分之一的样本,原研报将这种选股组合称为SMART组合。

回测规则:每月月底计算Q值、选股、调仓。等权重配置股票,即让每只股票的(价格*持有数)占总资产的比例相同。初始资金100万,交易费率0.003,选股空间、与上文相同。

为了方便回测,我们新建了一个仓位类,来对应每一组的账户。该类具有当前持有的股票列表、对应的价格、每只股票持有数量、现金、账户净值这些属性提供了更新价格、调整仓位的功能,并可以维护现金和账户净值。

class Position():
    '''
    为了方便回测,我们新建了一个仓位类,来对应每一组的账户。
    该类具有当前持有的股票列表、对应的价格、每只股票持有数量、现金、账户净值这些属性
    提供了更新价格、调整仓位的功能,并可以维护现金和账户净值
    '''
    def __init__(
        self,
        stocks=np.array([]),
        price=np.array([]),
        weight=np.array([]),
        cash=0
    ):
        self.stocks = stocks
        self.price = price
        self.weight = weight
        self.cash = cash
        if not self.stocks.size:
            self.net_value = self.cash
        else:
            self.net_value = (self.weight*self.price).sum()+self.cash
    def update_price(self,price):
        #传入一个价格序列,对应持有股票的最新价格
        for i,p in enumerate(price):
            if not np.isnan(p):
                self.price[i] = p
        self.net_value = (self.weight*self.price).sum()+self.cash
    def change_position(self,new_stocks,price,commission):
        #更新仓位,传入筛选出的股票列表和对应的价格
        
        for i,stock in enumerate(self.stocks):#原有股票不在新列表中,需要卖出
            if stock not in new_stocks:
                self.cash+=self.price[i]*self.weight[i]*(1-commission)
        
        #计算每一只股票应持有的数量
        new_weight = self.net_value*0.99/len(new_stocks)/price
        new_weight = [int(_) for _ in new_weight]
        
        for i,stock in enumerate(self.stocks):#原有股票在新列表中,需要调整数量
            if stock in new_stocks:
                weight_chg = new_weight[list(new_stocks).index(stock)] - self.weight[i]
                self.cash -= self.price[i]*weight_chg\
                              + abs(self.price[i]*weight_chg)*commission
                
        for i,stock in enumerate(new_stocks):#新增股票,需要买入
            if stock not in new_stocks:
                self.cash -= price[i]*new_weight[i]*(1+commission)
        self.stocks = new_stockps
        self.weight = new_weight
        self.price = price
        self.net_value = (self.weight*self.price).sum()+self.cash

多空对冲策略回测

capital_base = 1000000 #初始资金
commission = 0.003     #交易费率
#五组仓位
position_dict = {
    1:Position(cash=capital_base),
    2:Position(cash=capital_base),
    3:Position(cash=capital_base),
    4:Position(cash=capital_base),
    5:Position(cash=capital_base)
}
#净值统计
net_value_dict = {
    'month':[],#时间
    1:[],#每一组每个月月末的净值序列
    2:[],
    3:[],
    4:[],
    5:[]
}
trade_days = get_trade_days(start_date='2013-04-30', end_date='2016-05-31')
days_count = len(trade_days)
for i,date in enumerate(trade_days):#遍历交易日
    if  i == days_count-1 or trade_days[i+1].month != trade_days[i].month:
        #print(trade_days[i])
        #可交易股票
        stocks = get_stocks(trade_days[i],index='000300.XSHG')
        stocks_list = list(stocks.index)
        
        #股票的前10天的分钟数据,并根据股票分组
        data = get_minute_data(
            stocks_list,
            2400,
            end_dt=trade_days[i]
        ).reset_index(level=0).rename(columns={'level_0':'code'})
        data_groups = data.groupby('code')
        
        #计算每一只股票的Q值
        stocks['Q'] = [calcQ(data_bar) for name,data_bar in data_groups]
        #Q值排序
        stocks = stocks.sort_values(by='Q').reset_index()
        lens = stocks.shape[0]
        #根据Q值的大小等分为5组
        s_1 = stocks[stocks['Q'].isin(stocks['Q'][:int(0.2*lens)])].reset_index()
        s_2 = stocks[stocks['Q'].isin(stocks['Q'][int(0.2*lens):int(0.4*lens)])].reset_index()
        s_3 = stocks[stocks['Q'].isin(stocks['Q'][int(0.4*lens):int(0.6*lens)])].reset_index()
        s_4 = stocks[stocks['Q'].isin(stocks['Q'][int(0.6*lens):int(0.8*lens)])].reset_index()
        s_5 = stocks[stocks['Q'].isin(stocks['Q'][int(0.8*lens):])].reset_index()
        
        #如果有持仓数据
        if position_dict[1].stocks.size:
            #更新持仓价格
            for cls in range(1,6):
                    position_dict[cls].update_price(
                        price=get_price(                       
                            security = list(position_dict[cls].stocks),
                            count = 1,
                            end_date = trade_days[i],
                            panel = False
                        )['close']
                    )
        #调仓
        position_dict[1].change_position(new_stocks=s_1['index'],price = s_1['price'],commission=commission)            
        position_dict[2].change_position(new_stocks=s_2['index'],price = s_2['price'],commission=commission)
        position_dict[3].change_position(new_stocks=s_3['index'],price = s_3['price'],commission=commission)
        position_dict[4].change_position(new_stocks=s_4['index'],price = s_4['price'],commission=commission)
        position_dict[5].change_position(new_stocks=s_5['index'],price = s_5['price'],commission=commission)
        
        #添加该月的结果
        net_value_dict['month'].append(trade_days[i])
        for cls in range(1,6):
            net_value_dict[cls].append(position_dict[cls].net_value)
            continue
           
#可视化
net_value_df = pd.DataFrame(net_value_dict)
net_value_df.to_excel('net_value_df.xlsx')
net_value_df = pd.DataFrame(net_value_dict)
plt.figure(figsize=(14,6))
net_value_df[1] = (net_value_df[1]/1000000)
net_value_df[2] = (net_value_df[2]/1000000)
net_value_df[3] = (net_value_df[3]/1000000)
net_value_df[4] = (net_value_df[4]/1000000)
net_value_df[5] = (net_value_df[5]/1000000)
net_value_df['1与5对冲'] = net_value_df[1] - net_value_df[5]+1
index=get_bars(
            '000300.XSHG', 
            count = 37, 
            unit = '1M',
            fields = ['close'],
            df = True,
            include_now=True,
            end_dt = '2016-05-31'
        )['close']
index = index/index[0]
fig = plt.figure(figsize=(14,5))
ax = fig.add_subplot(111)
ax.plot(net_value_df['month'].index,net_value_df[1],label='1组')
ax.plot(net_value_df['month'].index,net_value_df[2],label='2组')
ax.plot(net_value_df['month'].index,net_value_df[3],label='3组')
ax.plot(net_value_df['month'].index,net_value_df[4],label='4组')
ax.plot(net_value_df['month'].index,net_value_df[5],label='5组')
ax.plot(net_value_df['month'].index,net_value_df['1与5对冲'],label='1组与5组对冲',linestyle='--',linewidth=3)
ax.plot(net_value_df['month'].index,index,linestyle='--',label='沪深300',color='black',linewidth=3)
ax.set_xlabel(u"时间")
ax.set_ylabel(u"净值")
ax.set_title(u"分组表现与多空对冲净值")
plt.xticks(net_value_df['month'].index,net_value_df['month'],rotation=90)
plt.legend()

图片

SMART选股策略

capital_base = 1000000
position_dict_2 = {
    1:Position(cash=capital_base)
}
net_value_dict_2 = {
    'month':[],
    1:[]
}

trade_days = get_trade_days(start_date='2013-04-30', end_date='2016-05-31')
days_count = len(trade_days)
commission = 0.003
for i in range(days_count):
    if i == days_count-1 or trade_days[i+1].month != trade_days[i].month:
        stocks = get_stocks(trade_days[i],index='000300.XSHG')
        stocks_list = list(stocks.index)
        data = get_minute_data(
            stocks_list,
            2400,
            end_dt=trade_days[i]
        ).reset_index(level=0).rename(columns={'level_0':'code'})
        data_groups = data.groupby('code')
        stocks['Q'] = [calcQ(data_bar) for name,data_bar in data_groups]
        stocks = stocks.sort_values(by='Q').reset_index()
        lens = stocks.shape[0]
        s_1 = stocks[stocks['Q'].isin(stocks['Q'][:int(0.2*lens)])].reset_index(drop=True)
        month_bar = get_bars(
            list(s_1['index']), 
            count = 1, 
            unit = '1M',
            fields = ['open','close'],
            df = True,
            include_now=True,
            end_dt = trade_days[i]
        ).reset_index(drop=True)
        
        #剔除涨幅过高的20%的股票
        s_1['ratio'] = (month_bar['close'] - month_bar['open'])/month_bar['open']
        s_1 = s_1.sort_values(by='ratio').reset_index(drop=True)
        lens = s_1.shape[0]
        s_1 = s_1[s_1['ratio'].isin(s_1['ratio'][:int(0.8*lens)])]
        
        
        if position_dict_2[1].stocks.size:
                position_dict_2[1].update_price(
                    price=get_price(                       
                        security = list(position_dict_2[1].stocks),
                        count = 1,
                        end_date = trade_days[i],
                        panel = False
                    )['close']
                )
        position_dict_2[1].change_position(new_stocks=s_1['index'],price = s_1['price'],commission=commission)            
        net_value_dict_2['month'].append(trade_days[i])
        net_value_dict_2[1].append(position_dict_2[1].net_value)
        
        
#可视化
net_value_df_2 = pd.DataFrame(net_value_dict_2)
net_value_df_2[1] = (net_value_df_2[1]/1000000)
index=get_bars(
            '000300.XSHG', 
            count = 37, 
            unit = '1M',
            fields = ['close'],
            df = True,
            include_now=True,
            end_dt = '2016-05-31'
        )['close']
index = index/index[0]
fig=plt.figure(figsize=(14,5))
ax = fig.add_subplot(111)
ax.plot(net_value_df_2['month'].index,net_value_df_2[1],label='SMART选股组合')
ax.plot(net_value_df_2['month'].index,index,label='沪深300')
ax.set_xlabel(u"时间")
ax.set_ylabel(u"净值")
ax.set_title(u"SMART选股组合")
plt.xticks(net_value_df_2['month'].index,net_value_df['month'],rotation=90)
plt.legend()

图片

4.总结

本文基本复现了【方正证券-跟踪聪明钱:从分钟行情数据到选股因子】的研究内容,同时得到了与原研报相近的结果。

本文的不足:

1.因为软硬件条件的限制,压缩了原研报的样本空间。

2.缺少原研报中因子风险特性的研究。

5.本文作者

蔡金航 哈尔滨工业大学威海校区 计算机科学与技术学院

舒意茗 哈尔滨工业大学威海校区 汽车工程学院

写在最后

我们是国内普通高校的在校学生,同时也是量化投资的初学者。我们的学校不是清北复交,也没有金融工程实验室,同时地处三线小城,因此我们在校期间较难获得量化实习机会,但我们期待与业界进行沟通、交流。

蔡金航同学是我们其中的一员。其在寻找暑期量化实习时,收到了几家私募和券商金工组的笔试邀请,笔试内容皆为在给定时间内复现出一篇金工研报。蔡同学受到启发,发觉复现金工研报是我们学习量化策略、锻炼程序设计能力同时也是与业界交流的很好的途径。

在蔡同学的建议下,我们开启研报复现系列的创作,记录我们的学习过程,并将我们的创作内容分享出来,与读者们一起交流、学习、进步。

我们的水平有限,创作的内容难免会有错误或不严谨的内容,我们欢迎读者的批评指正。

如果您对我们的内容感兴趣,请联系我们:[email protected]

你可能感兴趣的:(研报复现,python)