人工智能选股--股指期货对冲实现量化中性策略

我们的人工智能选股系列已经分享了很多篇,之前的回测实现都是纯多头策略,观察之前的回测结果可以发现,即使是收益最高的策略,策略收益也随指数一起经历了较高的波动和较大的回撤,而相比之下,策略相对基准的超额收益是比较稳定的,最大回撤也比较小。理论上相对于基准的超额收益才是我们的投资能力的真实体现,如果我们对自己的能力有信心,是不是可以考虑对冲掉指数的影响,只获取相对稳定的超额收益?这其实就是投资界常说的追求alpha收益。

詹森指数,又称为α 值,衡量投资组合与市场整体之间的绩效差异。它以资本资产定价模型为基础,根据 SML 来估计投资组合的超额收益率,反映了证券投资组合收益率与按该组合的 β系数算出来的均衡收益率之间的差额。计算公式为:

如果我们在做多股票的同时,按一定比例做空代表市场收益的股指期货,理论上我们就可以只获取alpha收益,而不承担市场风险,做空的比例即对冲比例(hedge ratio)。对冲比例主要取决于β

β代表投资组合收益对市场收益变动的敏感性,它的计算有两种方法,一种是使用公式计算:

另一种方法是用OLS对组合收益和指数收益做线性回归,以回归系数为β。

为了进行对冲,我们需要两个账户,一个股票账户和一个股指期货账户,股票账户和期货账户分设在证券公司和期货公司。假设股票账户中持有的股票总市值是S,股指期货账户持有的股指期货空单的总市值为F。那么当市场下跌比例为X时,股票组合的损失为βXS,同时股指期货空单的收益为XF。为使股指期货完全对冲市场波动带来的股票组合的损失,应满足βXS=XF,即F=βS

假设股指期货保证金要求的最低比例为m,则做空指数期货标的价值F的初始保证金为M1=mF=mβS

考虑到股票实行T+1制度,即卖出股票得到的钱需要第二天才能转出到期货账户。考虑极端情况,即大盘涨停,涨10%,则持有的空单F亏损了10%F,为避免爆仓应立即追加保证金M2=10%F=1/10βS。但如果遇到更极端的情况,第二天大盘开盘又涨停了怎么办?理论上,可以在大盘涨停的当天,卖出股票,第二天早上9:00将钱从股票账户转到期货账户,可以赶在9:15股指期货开盘之前完成。但若中间有任何问题,就赶不上了。所以,稳妥起见,追加保证金M2应留够2天的,也就是M2=1/5βS。在这种情况下,如果我们的总资金量为C,那么我们必须分配给期货账户的保证金为M=M1+M2=mβS+1/5βS,剩余的资金S全部分配给股票账户,即C=S+M=(1+mβ+1/5β)S,也可写作

上式中的分母部分即为资金分配比例。

股票资金S按比例投入到要买入的股票即可,而给定保证金M不需要全不投入到做空中,需要做空的股指期货合约的张数需要特别的计算逻辑。

指数期货标的价值F由指期货合约的张数N、指数价格indexprice和合约乘数multiplier三者相乘得到,即F=N*indexprice*multiplier,对冲股票资金S需要的指数期货标的价值F=βS,因此,需要做空的股指期货合约张数为:

下面我们以《人工智能选股--采样频率及调仓周期》第二部分“看长做短”的策略为基础,然后用中证500股指期货(IC)进行对冲。中证500股指期货的合约乘数固定为200,而它的保证金比例要求历史上发生过多次变化,为求简化,我们缩短回测时间,仅在2021年1月1日到2022年2月28日的时间区间进行回测,每周进行再平衡。将对冲前后的策略表现进行对比。

无对冲回测效果:

图1 无对冲回测效果图

有对冲回测效果:

图2 有对冲回测效果图

对比来看,对冲后的贝塔从0.928降低到了0.089,基本对冲掉了市场风险,策略波动率也从0.187下降到了0.1,整体风险也降低了。但与此同时,对冲后的策略收益也出现了大幅下降,这也符合预期,有两个显而易见的对冲成本导致了收益下降:一是因为要分配一部分资金到期货账户,可以投入到股票上的资金减少了很大一部分;而是因为期货买卖也带来了手续费支出,进一步拉低了策略收益。另外还有一个没那么明显的对冲成本,那就是股指期货长期贴水带来的做空成本。

图3 IC年化基差,数据来源:WIND,2015/4/16-2022/8/8

从上图可以看出,中证500股指期货(IC)长期处于贴水状态。事实上,不只IC,2015年股灾后,投资者的做空需求较大,股指期货贴水也迅速拉大。此后尽管贴水深度有所降低,但三大股指期货(IH、IF、IC)长期贴水情况一直延续至今。这一现象背后有多重原因,一个重要的原因是由于国内做空手段较少,相较于融券,利用股指期货进行多头对冲成本更低,近年来国内量化策略发展迅速,无论是量化对冲策略、指数增强策略,都将股指期货作为重要的对冲工具。这种多空力量失衡,导致了股指期货长期贴水。

因此股指期货对冲有利有弊,考虑到对冲的高昂成本,建议只在保证策略的Alpha 足够高并且稳定的前提下使用。

本文的实现在很大程度上参考了聚宽社区的“【量化课堂】股指期货对冲策略”及“‘【量化课堂】股指期货对冲策略’之学习笔记”。完整版代码如下:

import statsmodels.api as sm

from statsmodels import regression

from jqdata import *

import pandas as pd

import numpy as np

from six import BytesIO # 文件读取

from datetime import date

import datetime

from dateutil.relativedelta import relativedelta

enable_profile() # 开启性能分析

def initialize(context):

set_params()

set_variables()

set_backtest()

# 分仓

stock_cash = np.round(context.portfolio.starting_cash*(1/1.3),0)

future_cash = context.portfolio.starting_cash - stock_cash

set_subportfolios(

[

SubPortfolioConfig(cash=stock_cash, type='stock'),

SubPortfolioConfig(cash=future_cash,type='index_futures')

]

)

run_weekly(Trade, 1, time='open', reference_security='000905.XSHG')

def set_params():

g.result_df = pd.read_csv(

#BytesIO(read_file('DataNew/result_df_20_xgboost72-fpr-proba.csv')), index_col=[0])

#BytesIO(read_file('DataNew/result_df_xgb_60_long_week_20_lessmf_bl.csv')), index_col=[0])

BytesIO(read_file('DataNew/result_df_20_xgboost72R-scale-long.csv')), index_col=[0])

#g.result_df = pd.read_csv('Data/result1_df.csv', index_col=0, parse_dates=True)

g.yb=63      # 样本长度

g.pre_future=''#用来装上次进入的期货合约名字

def set_variables():

g.in_position_stocks = [] #持仓股票

def set_backtest():

set_option("avoid_future_data", True)  # 避免数据

set_option("use_real_price", True)  # 真实价格交易

set_benchmark('000905.XSHG')  # 设置基准

#log.set_level("order", "debuge")

log.set_level('order', 'error')

# 每日盘前运行

def before_trading_start(context):

# 手续费设置

# 将滑点设置为0

set_slippage(FixedSlippage(0))

# 根据不同的时间段设置手续费

dt = context.current_dt

set_commission(PerTrade(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))

# 设置期货合约保证金

g.futures_margin_rate = 0.12 

set_option('futures_margin_rate', g.futures_margin_rate)

def Trade(context):

#bar_time = context.current_dt.strftime('%Y-%m-%d')

bar_time = context.previous_date.strftime('%Y-%m-%d')

log.info('%s启动' % bar_time)

if bar_time in g.result_df.index:

print('存在')

g.in_position_stocks = g.result_df.loc[bar_time]['code']

print(g.in_position_stocks)

if len(g.in_position_stocks)>0:

print(g.in_position_stocks)

# 计算对冲比例和 beta

hedge_ratio, beta = compute_hedge_ratio(context, g.in_position_stocks)

# 调仓

rebalance(hedge_ratio, beta, context)

# 7 

# 计算对冲比例

# 输出两个 float

def compute_hedge_ratio(context, in_position_stocks):

prices = history(g.yb, '1d', 'close', in_position_stocks)

index_prices = attribute_history('000905.XSHG', g.yb, '1d', 'close')

# prices 行:日期,列:各只股票 =>pct_change():dataframe, 结构不变,值为日收益率=>[1:] drop first row

# =>mean(axis=1)横向平均,Series=>.values:array

mean_rets = prices.pct_change()[1:].mean(axis=1).values

# pct_change():dataframe, 结构不变,值为日收益率=>[1:] drop first row=>.close:Series =>values:array

index_rets = index_prices.pct_change()[1:].close.values

# 计算组合和指数的协方差矩阵

index_rets = sm.add_constant(index_rets)    # 常数用来拟合alpha,系数用来拟合beta

model = regression.linear_model.OLS(mean_rets, index_rets).fit()  #线性回归,OLS普通最小二乘法ordinary least square

alpha, beta = model.params[0], model.params[1]

# 计算并返回对冲比例

return 1+beta*g.futures_margin_rate+beta/10, beta

# 8

# 调仓函数

# 输入对冲比例

def rebalance(hedge_ratio, beta, context):

# 计算资产总价值

total_value = context.portfolio.total_value

# 计算预期的股票账户价值

expected_stock_value = total_value/hedge_ratio

# 将两个账户的钱调到预期的水平

transfer_cash(1, 0, min(context.subportfolios[1].transferable_cash, max(0, expected_stock_value-context.subportfolios[0].total_value)))

transfer_cash(0, 1, min(context.subportfolios[0].transferable_cash, max(0, context.subportfolios[0].total_value-expected_stock_value)))

# 计算股票账户价值(预期价值和实际价值其中更小的那个)

stock_value = min(context.subportfolios[0].total_value, expected_stock_value)

# 计算相应的期货保证金价值

futures_margin = stock_value * beta * g.futures_margin_rate

# 调整股票仓位,在 g.in_position_stocks 里的等权分配

for stock in context.subportfolios[0].long_positions.keys():

if stock not in g.in_position_stocks:

order_target(stock,0,pindex=0)  

curr_data = get_current_data()

target_stocks = [stock for stock in g.in_position_stocks if not curr_data[stock].paused ] #过滤掉今日停牌的

per_value = stock_value/len(g.in_position_stocks) #每只股票应该达到的权值    

over_weight_list  = [stock for stock in target_stocks if \

context.subportfolios[0].long_positions[stock].value > per_value]  #现持仓中超权的

under_weight_list = [stock for stock in target_stocks if \

stock not in over_weight_list]  #剩余的,就是贴权的,应该补权

for stock in over_weight_list:      # 超权的先减仓,削高

order_target_value(stock, per_value, pindex=0)

for stock in under_weight_list:     # 贴权的再加仓,填低

order_target_value(stock, per_value, pindex=0)

# 获取下月连续合约 string

current_future = get_next_month_future(context,'IC')

# 如果下月合约和原本持仓的期货不一样

if g.pre_future!='' and g.pre_future!=current_future:

# 就把仓位里的期货平仓

order_target(g.pre_future, 0, side='short', pindex=1)

# 现有期货合约改为刚计算出来的

g.pre_future = current_future

# 获取中证500价格

index_price = attribute_history('000905.XSHG',1, '1d', 'close').close.iloc[0]

# 计算并调整需要的空单仓位

order_target(current_future, int(futures_margin/(index_price*200*g.futures_margin_rate)), side='short', pindex=1)

# 9

# 取下月连续string

# 输入 context 和一个 string,后者是'IF'或'IC'或'IH'

#IF = Index Future = Index of HS300

#IC = Index China = Index of ZZ500

#IH = Index Hu(沪) = Index of SZ50

# 输出一 string,如 'IF1509.CCFX'

# 进入本月第三周即切换到下月合约,而不等第三周的周五本月合约结束

def get_next_month_future(context, symbol):

dt = context.current_dt

month_begin_day = datetime.date(dt.year, dt.month, 1).isoweekday() # 本月1号是星期几(1-7)

third_monday_date = 16 - month_begin_day + 7*(month_begin_day>5) #本月的第三个星期一是几号

# 如果今天没过第三个星期一

if dt.day < third_monday_date:

next_dt = dt #本月合约

else:

next_dt = dt + relativedelta(months=1)  #切换至下月合约

year = str(next_dt.year)[2:]

month = ('0' + str(next_dt.month))[-2:]

return (symbol+year+month+'.CCFX')

#量化交易 #对冲 #风险中性

如果你对因子投资、量化择时等理论方法感兴趣,欢迎查看至简量化主页置顶的那本书--《教你玩转量化交易》,系统梳理了中低频量化交易的核心方法,适合量化投资者学习使用。

两年时间,我写了本股市量化交易的书

# 芝士小课堂

免责声明:上述内容仅代表发帖人个人观点,不构成本平台的任何投资建议。

举报

评论

  • 推荐
  • 最新
empty
暂无评论