中金公司《高频因子手册》分享3:波动率因子,从构建到回测
  迪仔 2025年06月09日 161 0

上一篇文章我们介绍了高频因子的动量反转类因子,这一篇继续介绍波动率因子,并在因子分析的基础上加入策略回测。
研究环境利用聚宽因子分析API,构建因子函数类;研究在日内高频分钟级数据中挖掘构建高频因子,对该因子进行有效性检验,并利用回测平台进行回测。

一 波动率因子

1.1 波动率因子构建

第二大类因子为波动率因子。波动率因子刻画了股票价格或股票收益在过去一段时间的不确定性程度,高波动率通常反映其不确定性程度较高,未来收益表现可能相对较弱。

pic1.png

将传统的收益波动、振幅因子应用到日内分钟数据中,可以构建 7 个高频波动率因子。vol_upVol_std 因子在大部分市场中有效性良好。收益波动率因子包括传统的分钟波动率因子、上行波动率因子、下行波动率因子。对比收益波动率因子表现,不难看出,上行波动率因子有效性优于分钟 k 线波动率因子、下行波动率因子,全市场 ICIR 绝对值可达 1.35,IC 均值可达-8.4%。

1.2 波动率因子表现

pic2.png

部分波动率因子表现优秀但回撤明显。

pic3.png

波动率因子之间的相关性统计如下表:

pic4.png

全市场范围内, vol_volume1min_o 等因子多空和多头超额表现较好,且单调性较为良好,但从多空收益曲线来看稳定性不足。vol_return1min_o 因子周度多空年化收益率 41%,年化多头超额一般。周度换仓年化超额收益最高的因子为 vol_downRatio_o,多头超额 14%,稳定性也欠佳,多头稳定性较好的因子为 vol_range1min_std。

pic5.png

二 因子复现

下面我们选取一个具体因子来进行构建和回测,使用聚宽研究环境。选取因子为vol_return1min_std,构建方式为日内分钟收益率的标准差,最后的后缀“_std”表式对调仓周期取当期标准差。
使用因子分析API,在研究环境中调用以进行因子分析。API使用详情可参阅聚宽技术文档。

# 构建波动率高频因子计算函数 class vol_return1min_std(Factor): # 设置因子名称 name = 'vol_return1min_std' # 设置获取数据的时间窗口长度 max_window = 1 # 设置依赖的数据 dependencies = ['close'] #参考标的 # 计算因子的函数, 需要返回一个 pandas.Series, index 是股票代码,value 是因子值 def calc(self, data): # 获取日期,股票池 date= data['close'].index[0] #获取(上一)交易日 实际上的逻辑是用上一交易日的数据在今天进行交易,时间戳是昨天的时间 security_list = data['close'].columns.tolist() # 以下为获取更多数据 #获取分钟级数据,我们取前20日平均值作为当期的因子值 stock_bars = get_price(security_list,count =240 * g.R,start_date=None, end_date=date, frequency='1m', fields=['close'], skip_paused=False, fq='none') stock_bars['close'] = stock_bars['close'].fillna(method='ffill') stock_bars['return'] = np.log(stock_bars['close']/stock_bars['close'].shift(1)) stock_bars['return'] = stock_bars['return'].fillna(0) stock_close = stock_bars['return'] #以日为单位迭代计算当日因子值 stock_close['time'] = [i.time() for i in stock_close.index] stock_close.index = [i.date() for i in stock_close.index] stock_close['day'] = stock_close.index GroupBy = stock_close.groupby(by='day') vol_return = pd.DataFrame() vol_return_temp = pd.DataFrame() for day, group in GroupBy: group = group.drop(['day','time'], axis=1) temp = self.cal_factor(group) vol_return[day] = temp['factor'] vol_return_temp['factor'] = vol_return.apply(np.std, axis=1) vol_return_temp = vol_return_temp.dropna(axis=0) vol_return_temp = vol_return_temp.sort_values('factor',ascending = True) return vol_return_temp['factor'] ## 计算因子值 def cal_factor(self,stock_bar): rtn_vol = pd.DataFrame() rtn_vol['factor']=stock_bar.std() return rtn_vol

代码构建完成后先取任一段时间检查一下计算结果:
pic.png

三 因子分析

3.1 引擎初始化

下面我们对上面构建的波动率因子进行回测分析,从各个方面考察因子的有效性。为保持与研报的一致性,我们主要考察周度和月度调仓频率下的表现。
参数设置如下:
(1)测试时间:2024-01-01 至 2025-06-05;
(2)分位数:十分位数;
(3)调仓周期:5日、30日;
(4)仓位配置:等权配置;
(5)股票池:沪深300

start = datetime.datetime.today() far = analyze_factor( vol_return1min_std(), start_date='2024-01-01', end_date='2025-06-05', universe='000300.XSHG', quantiles=10, periods=(5,30), industry='jq_l1', weight_method='avg', use_real_price=True, skip_paused=False, max_loss=0.2 ) end = datetime.datetime.today() print('程序耗时:', end - start)

3.2 因子IC分析

首先我们直观的看下收益分层分布的情况。总的来看越靠前的收益越高,符合预期。

pic6.png

然后使用IC信息系数法对趋势强度因子进行检验。我们排除因子值大小的影响,使用RankIC来替代IC值进行分析。RankIC为当期因子值的排名与下一期因子值排名的相关性。
下面我们绘制因子5日、30日的RankIC时间序列图和月度均值图。

pic7.png
pic8.png

在 2024-01到2025-05这17个月中,5、30日IC均值分别为为-0.065、-0.118,说明因子值与下月收益率呈现一定的负相关性,即因子值越低,下月预期收益率越高。
我们考察5、30日因子的IR值,分别为0.254、0.218。可见,5日换仓和30日换仓因子稳定性相近,30日更好。
进一步的计算IC_IR值为:-0.256、-0.541。因此,在单位风险下,30日换仓因子具有最佳的有效性。

四 策略回测

4.1 构建策略

根据上述分析,我们构建因子的分层回测组合,用于考察因子实际的选股能力。这一环节我们使用真实费率。
股票池:沪深300/中证800/中证1000,剔除 ST、停牌、上市时间小于 6 个月的股票
回测时间:2020-01-01 至 2025-05-31
调仓期:每30天
费率:买入时佣金万分之三,卖出时佣金万分之三加千分之一印花税, 每笔交易佣金最低扣5元
选股步骤可分为以下三部分:
(1)在每个调仓日第一天计算因子值
(2)对因子值根据从小到大的顺序进行排序,并将其等分为 10 组
(3)每个调仓日选取第一组(前10%)股票池进行调仓交易
(4)作为对比,每个调仓日选取第10组(后10%)再次进行回测

# 导入函数库 from jqdata import * import datetime import pandas as pd import statsmodels.api as sm from statsmodels import regression from statsmodels.formula.api import ols from jqfactor import Factor, calc_factors from jqfactor import neutralize from jqfactor import winsorize_med from jqfactor import standardlize # 初始化函数,设定基准等等 def initialize(context): # 设定沪深300作为基准 set_benchmark('000300.XSHG') # 开启动态复权模式(真实价格) set_option('use_real_price', True) # 输出内容到日志 log.info() log.info('初始函数开始运行且全局只运行一次') # 过滤掉order系列API产生的比error级别低的log # log.set_level('order', 'error') ### 股票相关设定 ### # 股票类每笔交易时的手续费是:买入时佣金万分之三,卖出时佣金万分之三加千分之一印花税, 每笔交易佣金最低扣5块钱 set_order_cost(OrderCost(close_tax=0.001, open_commission=0.00025, close_commission=0.00025, min_commission=5),type='stock') ## 运行函数:每月第一天开盘后进行调仓 run_monthly(market_open, monthday=1, time='open', reference_security='000300.XSHG') #run_daily(market_open, time='open', reference_security='000300.XSHG') ### 变量初始化 ### g.index = ['000300.XSHG']#['000852.XSHG','000906.XSHG'] g.N = 6 g.num = 10 #此处设置选择第一组或第十组 g.R = 20 g.timer = 1 ''' ######################策略的交易逻辑###################### 每周计算因子值, 并买入前 10% 股票 ''' # 每周或每月开盘运行一次, 按照上一交易日的因子值进行调仓 def market_open(context): if g.timer % 10 == 1: log.info('函数运行时间(market_open):'+str(context.current_dt.time())) # 1. 定义计算因子的 universe, # 建议使用与 benchmark 相同的指数,方便判断选股带来的 alpha #universe = get_index_stocks('000300.XSHG') security_list = get_qualified_stocks(context) # 2. 获取因子值 # get_factor_values 有三个参数,context、因子列表、股票池, # 返回值是一个 dict,key 是因子类的 name 属性,value 是 pandas.Series # Series 的 index 是股票代码,value 是当前日期能看到的最新因子值 factor_values = get_factor_values(context, [vol_return1min_std()], security_list) # 3. 对因子做线性加权处理, 并将结果进行排序。您在这一步可以研究自己的因子权重模型来优化策略结果。 # 对因子做 rank 是因为不同的因子间由于量纲等原因无法直接相加,这是一种去量纲的方法。 #final_factor = vol_return1min.rank(ascending=False) vol_return1min = factor_values['vol_return1min_std'] # 4. 由因子确定每日持仓的股票列表: # 采用因子值由大到小排名前 10% 股票作为目标持仓 stock_list, buy_stock_count = select_stock_universe(context, vol_return1min, g.num) # 5. 根据股票列表进行调仓: # 这里采取所有股票等额买入的方式,您可以使用自己的风险模型自由发挥个股的权重搭配 rebalance_position(context, stock_list, buy_stock_count) g.timer += 1 ''' ######################下面是策略中使用的因子###################### 可以先使用因子分析功能生产出理想的因子, 再加入到策略中 因子分析:https://www.joinquant.com/algorithm/factor/list ''' # 构建波动率高频因子计算函数 class vol_return1min_std(Factor): # 设置因子名称 name = 'vol_return1min_std' # 设置获取数据的时间窗口长度 max_window = 1 # 设置依赖的数据 dependencies = ['close'] #参考标的 # 计算因子的函数, 需要返回一个 pandas.Series, index 是股票代码,value 是因子值 def calc(self, data): # 获取日期,股票池 date= data['close'].index[0] #获取(上一)交易日 实际上的逻辑是用上一交易日的数据在今天进行交易,时间戳是昨天的时间 security_list = data['close'].columns.tolist() # 以下为获取更多数据 #获取分钟级数据,我们取前20日平均值作为当期的因子值 stock_bars = get_price(security_list,count =240 * g.R,start_date=None, end_date=date, frequency='1m', fields=['close'], skip_paused=False, fq='none') stock_bars['close'] = stock_bars['close'].fillna(method='ffill') stock_bars['return'] = np.log(stock_bars['close']/stock_bars['close'].shift(1)) stock_bars['return'] = stock_bars['return'].fillna(0) stock_close = stock_bars['return'] #以日为单位迭代计算当日因子值 stock_close['time'] = [i.time() for i in stock_close.index] stock_close.index = [i.date() for i in stock_close.index] stock_close['day'] = stock_close.index GroupBy = stock_close.groupby(by='day') vol_return = pd.DataFrame() vol_return_temp = pd.DataFrame() for day, group in GroupBy: group = group.drop(['day','time'], axis=1) temp = self.cal_factor(group) vol_return[day] = temp['factor'] vol_return_temp['factor'] = vol_return.apply(np.std, axis=1) vol_return_temp = vol_return_temp.dropna(axis=0) vol_return_temp = vol_return_temp.sort_values('factor',ascending = True) return vol_return_temp['factor'] ## 计算因子值 def cal_factor(self,stock_bar): rtn_vol = pd.DataFrame() rtn_vol['factor']=stock_bar.std() return rtn_vol """ ###################### 工具 ###################### """ ## 开盘前运行函数 def before_trading_start(context): # 输出运行时间 log.info('函数运行时间(before_market_open):'+str(context.current_dt.time())) # 给微信发送消息(添加模拟交易,并绑定微信生效) # send_message('美好的一天~') ## 收盘后运行函数 def after_trading_end(context): log.info(str('函数运行时间(after_market_close):'+str(context.current_dt.time()))) #得到当天所有成交记录 trades = get_trades() for _trade in trades.values(): log.info('成交记录:'+str(_trade)) log.info('一天结束') log.info('##############################################################') ## 选股函数 def get_qualified_stocks(context): #获取前一个交易日日期 previous_date = context.previous_date #获取全体A股 stock_list = [] for index in g.index: stock_list += get_index_stocks(index_symbol=index, date=previous_date) #剔除ST st_list = get_extras('is_st', stock_list, count=1, end_date=previous_date) stock_list = [stock for stock in stock_list if not st_list[stock][0]] #剔除上市不足N个月新股 stock_list_fn = [] for stock in stock_list: start_date = get_security_info(stock).start_date if start_date < previous_date - datetime.timedelta(days=g.N*30): stock_list_fn.append(stock) #剔除停牌股票 paused_series = get_price(stock_list_fn,end_date=context.current_dt,count=1,fields='paused')['paused'].iloc[0] stock_list_fn = paused_series[paused_series==False].index.tolist() return stock_list_fn ## 待选股票池 def select_stock_universe(context, factor_df, num): length = int(len(factor_df.index) / 10) stock_list = factor_df.index[length*(num-1):length*num] return (stock_list.tolist(),length) """ 调仓: 先卖出持仓中不在 stock_list 中的股票 再等价值买入 stock_list 中的股票 """ ## 调仓 def rebalance_position(context, buy_stocks, buy_stock_count): # 现持仓的股票,如果不在“目标池”中,且未涨停,就卖出 if len(context.portfolio.positions)>0: last_prices = history(1, '1m', 'close', security_list=context.portfolio.positions.keys()) for stock in context.portfolio.positions.keys(): if stock not in buy_stocks : curr_data = get_current_data() if last_prices[stock][-1] < curr_data[stock].high_limit: order_target_value(stock, 0) # 依次买入“目标池”中的股票 for stock in buy_stocks: position_count = len(context.portfolio.positions) curr_data = get_current_data() last_prices = history(1, '1m', 'close', security_list=buy_stocks) #if get_growth_rate(g.indexNew,10) >= 0.01: if buy_stock_count > position_count: value = context.portfolio.cash / (buy_stock_count - position_count) if context.portfolio.positions[stock].total_amount == 0 and last_prices[stock][-1] < curr_data[stock].high_limit: order_target_value(stock, value) """ # 策略中获取因子数据的函数 每日返回上一日的因子数据 详见 帮助-单因子分析 """ def get_factor_values(context,factor_list, universe): """ 输入: 因子、股票池 返回: 前一日的因子值 """ # 取因子名称 factor_name = list(factor.name for factor in factor_list) # 计算因子值 values = calc_factors(universe, factor_list, context.previous_date, context.previous_date) # 装入 dict factor_dict = {i:values[i].iloc[0] for i in factor_name} return factor_dict

4.2 策略回测结果分析

沪深300股票池前10%策略收益16.95%,后10%策略收益-11.41%,同期基准收益-6.26%。在2021年以前第一组和第10组曲线差别不大,2021年后分层效果较好。

zz300_2.png
zz300.png

中证800股票池前10%策略收益16.12%,后10%策略收益-2.50%,同期基准收益-3.23%。整体分层效果较好,多头组合整体上明显跑赢空头组合。

zz800_2.png
zz800.png

再看一下中证1000股票池,前10%策略收益40.95%,后10%策略收益27.34%,同期基准收益8.25%。分层效果较好。两个分组收益都超过基准,可能与市值因素相关。后续可以做一些中性化处理并尝试不同的分组。

zz1000_2.png
zz1000.png

五 总结

我们通过对所选取的波动率因子的有效性分析以及分层回测检验,初步得到以下结论:在 2024-01到2025-05中,因子值与下月收益率呈现一定的负相关性,我们考察因子的IR值,30日换仓因子稳定性较好,且在单位风险下,30日换仓因子具有最佳的有效性。
继续构建策略选取前10%股票进行回测,选取五年数据。从分组回测来看,分层效果较好。多头组合整体上明显跑赢空头组合,符合单因子有效性的检验。

最后一次编辑于 9天前 1

暂无评论

推荐阅读