人工智能选股--股指期货对冲实现量化中性策略
我们的人工智能选股系列已经分享了很多篇,之前的回测实现都是纯多头策略,观察之前的回测结果可以发现,即使是收益最高的策略,策略收益也随指数一起经历了较高的波动和较大的回撤,而相比之下,策略相对基准的超额收益是比较稳定的,最大回撤也比较小。理论上相对于基准的超额收益才是我们的投资能力的真实体现,如果我们对自己的能力有信心,是不是可以考虑对冲掉指数的影响,只获取相对稳定的超额收益?这其实就是投资界常说的追求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')
#量化交易 #对冲 #风险中性
如果你对因子投资、量化择时等理论方法感兴趣,欢迎查看至简量化主页置顶的那本书--《教你玩转量化交易》,系统梳理了中低频量化交易的核心方法,适合量化投资者学习使用。
两年时间,我写了本股市量化交易的书
免责声明:上述内容仅代表发帖人个人观点,不构成本平台的任何投资建议。
