研究对象:
【量化课堂】股指期货对冲策略
研究目的:
- 寻求一套通用的对冲例程,以期望用于各种资产组合。
- 学习大神编程经验,提高自己的python语言水平。
研究过程:
一、重写原版
发现get_next_month_future函数逻辑有些乱,改写了该函数。
# 进入本月第三周即切换到下月合约,而不等第三周的周五本月合约结束 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')
重写compute_hedge_ratio函数
计算资产组合及沪深300指数的日收益率:
原算法:# 取股票在样本时间内的价格 prices = history(g.yb, '1d', 'close', in_position_stocks) # 取指数在样本时间内的价格 index_prices = list(attribute_history('000300.XSHG', g.yb, '1d', 'close').close) # 计算股票在样本时间内的日收益率 rets = [(prices.iloc[i 1,:]-prices.iloc[i,:])/prices.iloc[i,:] for i in range(g.yb-1)] # 计算日收益率平均 mean_rets = [np.mean(x) for x in rets] # 计算指数的日收益率 index_rets = [(y-x)/x for (x,y) in zip(index_prices[:-1],index_prices[1:])]
修改后简洁地表达:
prices = history(g.yb, '1d', 'close', in_position_stocks) index_prices = attribute_history('000300.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
并对beta计算过程进行了注释:
# 计算组合和指数的协方差矩阵cov_mat # Rp | Rm # Rp Rp.Var | cov(Rp,Rm) # Rm cov(Rm,Rp) | Rm.Var cov_mat = np.cov(mean_rets, index_rets) # 计算组合的系统性风险beta beta = cov_mat[0,1]/cov_mat[1,1] ''' # 另一种算法 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] '''
本函数返回对冲比例和beta值,其中对冲比例hedge_ratio的表达式
1 beta*g.futures_margin_rate beta/5
这个对冲比例hedge_ratio怎么来的?有什么用?请看下面的“研究”重写initialize函数:
原表达:set_subportfolios([SubPortfolioConfig(cash=context.portfolio.starting_cash*(1/1.3) ,type='stock'),SubPortfolioConfig(cash=context.portfolio.starting_cash*0.3/1.3,type='index_futures')])
现表达:
# 分仓 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') ] )
重写before_trading_start函数
注释掉了如下一条语句,并将之移到compute_signals函数里:g.all_stocks = set_feasible_stocks(get_all_securities(['stock']).index,g.yb,context)
理由是:每天计算g.all_stocks计算量极大。实际上只有在compute_signals才能用上g.all_stocks,
而compute_signals函数20个交易日才执行一次(g.tc=20 #调仓频率),所以每天都计算g.all_stocks是无用功。重写compute_signals函数:
原写法:if g.t%g.tc==0: # 获取所有股票的财务数据总负债和总资产 q = query(balance.code, balance.total_liability, balance.total_assets).filter(balance.code.in_(g.all_stocks)) data = get_fundamentals(q) # 计算资产负债比 data['ALR'] = data['total_liability']/data['total_assets'] # 资产负债比从大到小排列 data = data.sort('ALR', ascending=False) # 输出最靠前的 3% return list(data.code)[:int(float(len(g.all_stocks))*g.percentile)]
修改后:
if g.t%g.tc==0: # 获取可行股票池 all_stocks = set_feasible_stocks(get_all_securities(['stock']).index,g.yb,context) # 获取所有股票的财务数据总负债和总资产 q = query( balance.code, balance.total_liability, balance.total_assets, (balance.total_liability/balance.total_assets).label('ALR') ).filter( balance.code.in_(all_stocks) ).order_by( (balance.total_liability/balance.total_assets).desc() #按资产负债率降序 ) data = get_fundamentals(q) stock_list = data['code'].tolist() # 输出最靠前的 3% return stock_list[:int(len(all_stocks)*g.percentile)]
修改后的代码试图用一条sql查询语句找出想要的答案。
修改时发现了一个问题,就是“资产负债率”按大到小排列?
一般地,资产负债率越高,表明该企业已经陷入或即将陷入财务困境,这样的股票表现能好么?
为忠实再现原作,先不改,保持降序。策略不是重点,重点是对冲效果。修改rebalance函数
原作183-186行for stock in g.in_position_stocks: order_target_value(stock, stock_value/len(g.in_position_stocks), pindex=0) for stock in g.in_position_stocks: order_target_value(stock, stock_value/len(g.in_position_stocks), 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)
为更精准地对冲,从获取沪深300价格改为获取股指期货价格,原因:
(1)股指期货与现货相比,存在升水或贴水
(2)对冲的不限于IF,也可能是IH,IC# 获取沪深300价格 # index_price = attribute_history('000300.XSHG',1, '1d', 'close').close.iloc[0] # log.info('HS300 index_price: %.2f' % index_price) # 获取期货指数价格 index_price = attribute_history(current_future, 1, '1d', 'close').close.iloc[0] log.info('Index futures: %s, Price: %.2f' % (current_future, index_price))
rebalance函数中还有几处费解的地方,如何去理解,请看下面的“研究”
另外,对rebalance函数进行了详尽的log,以解释各个变量的含义,并可以借此检验是否达到对冲要求。
一个教学策略,我花了2天的时间来学习,受益匪浅。原作水平很高,尤其是rebalance函数,写的很精彩!
回测的时候发现,起始资金2个亿,有些多了,即使一次买N多只股票,依旧有超出该股全天成交量的。故把起始资金调整为2千万。
资金量小了,股指期货对冲的精度就下降了,所以,回测的结果与原作会有不同。
先改到这里,看看回测效果再说。嗯,回测速度快了很多!
研究过程¶
二、理解beta、对冲比例hedgeRatio和调仓函数rebalance¶
1. $\beta$的计算¶
根据资本资产定价模型(CAPM),资产组合(portfolio)收益$R_p$与市场收益$R_m$之间的关系,可以表达为:
$$ R_p = \alpha_p + \beta_p R_m $$$\beta$ 计算方法1:用公式直接计算
$$\beta_p = \frac{Cov(R_p, R_m)}{Var(R_m)}$$ # 计算组合和指数的协方差矩阵
# 函数np.cov(), 返回值shape(2*2)的np.array
# | Rp | Rm
# ----------------------------------
# Rp | Var(Rp) | cov(Rp,Rm)
# Rm | cov(Rm,Rp) | Var(Rm)
cov_mat = np.cov(portfolio_Rets, index_Rets)
# 计算组合的系统性风险beta
beta = cov_mat[0,1]/cov_mat[1,1]
$\beta$ 计算方法2:线性回归
index_Rets = sm.add_constant(index_Rets) # 常数用来拟合alpha,系数用来拟合beta
#线性回归,OLS普通最小二乘法ordinary least square
model = regression.linear_model.OLS(portfolio_Rets, index_Rets).fit()
alpha, beta = model.params[0], model.params[1]
2. $\beta$的含义¶
由上述CAMP公式可以得到:
$$ \Delta R_p = \beta \Delta R_m $$即市场波动1%, 资产组合的收益将波动 $\beta$ X 1% . $\beta$代表了资产组合收益对市场收益变动的敏感性。
3.期现对冲¶
由上述$\beta$的含义可知,若资产组合的总价值为S,则市场下跌$X$时,损失为:
$$ Loss = \beta X S $$为避免此损失,在持有资产组合的同时,持有股指期货的空单,设空单的市值为F,则市场下跌$X$时,收益为:
$$ Gain = XF $$为对冲资产组合的损失,令:Gain = Loss , 即:
$$ XF = \beta X S $$亦即持有的股指期货空单市值F应为:
$$ F = \beta S $$4. 头寸初步配置¶
设股指期货保证金比例为m,则做空指数期货标的价值F的初始保证金M为:
$$ M=mF=m\beta S $$将总现金C在股票S和期货保证金M间分配:
$$ C = S + M = S + m\beta S = (1+m\beta)S $$所以,股票S至多持有:
$$ S = \frac{C}{1+m\beta} $$5.头寸配置的进一步考量:若大盘连续两日涨停?¶
制度背景:股票账户和期货账户分设在证券公司和期货公司,且股票实行T+1制度,即卖出股票得到的钱需要第二天才能转出。
前文已述,做空标的价值F的股指期货,需要初始保证金$M_1$:
$$M_1 = m\beta S$$假设极端情况,即大盘涨停,涨10%,则持有的空单F亏损了10%F,应立即追加保证金$M_2$:
$$M_2 = 10\%F = \frac{1}{10} \beta S$$即使立即卖出股票,也不能用卖出股票的钱来追加保证金,为避免期货账户爆仓,期货账户的保证金M应至少为:初始保证金M1 + 追加保证金M2。
$$ M = M_1 + M_2 = m\beta S + \frac{1}{10} \beta S $$好,那就假设我的期货账户保证金 M 达到了上述金额,够了么?若再遇到500年不遇的行情,第二天大盘开盘又涨停了呢?
理论上,可以在大盘涨停的当天,卖出股票,第二天早上9:00将钱从股票账户转到期货账户,可以赶在9:15股指期货开盘之前完成。但若中间有任何问题,就赶不上了。所以,保险一点,追加保证金$M_2$留够2天的,即:
$$ M = M_1 + M_2 = m\beta S + 2 \times \frac{1}{10} \beta S = m\beta S + \frac{1}{5} \beta S $$再来分配一下资金:
$$ C = S + M = S + m\beta S + \frac{1}{5} \beta S = (1 + m\beta + \frac{1}{5} \beta)S $$$$ S = \frac{C}{1 + m\beta + \frac{1}{5} \beta} $$将上式中的分母定义为“对冲比例”(hedge ratio):
$$ S = \frac{C}{hedgeRatio} $$$$ hedgeRatio = 1 + m\beta + \frac{1}{5} \beta $$看到这,再联想到量化教程里面那个奇怪的对冲比例:
1 + beta*g.futures_margin_rate + beta/5
就可以理解它的来历了。
到此,写了这么长,理解了一个量化教程里面的一个函数compute_hedge_ratio
的两个返回值:beta, hedge_ratio
这是第一次用Markdown写数学公式,找了篇教程看了看,就动手写,觉得很好玩。
6. 解剖rebalance函数¶
def rebalance(hedge_ratio, beta, context):
# 计算资产总价值
total_value = context.portfolio.total_value
# 计算预期的股票账户价值
expected_stock_value = total_value/hedge_ratio
totaol_value是所有仓位,包括股票多仓subportfolios[0],以及期货空仓subportfolios[1]在内的总价值。
expected_stock_value,即股票仓位的合理值S,回顾前文中写过的那个公式:
$$ S = \frac{C}{hedgeRatio} $$ # 将两个账户的钱调到预期的水平
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
先看看股票账户相关的两条语句:
transfer_cash(0,1,........), 是将现在股票仓位subportfolios[0]超出合理值S的部分调出到期货账户1中,但可能调不出那么多,需要今天卖股票,现在能调出的额度,受限于股票账户可动用现金transferable_cash。
stock_value = min(......), 可以动用的股票资金,是股票现持仓H和股票合理价值S中二者的最小值,即如果H > S,那么多出的部分不能动用,是应该调出的,如果H < S,应该调入,但现在还没有到位,只能是现在有多少钱就用多少钱。
再看看期货账户相关的两条语句:
transfer_cash(1,0,........),是期货账户subportfolios[1]中的钱调往股票账户0中,股票账户现值subportfolios[0].total_value低于股票仓位合理值S了,则说明期货账户钱多了,应将股票账户补到合理值S,但同样也受限于现在期货账户中可以动用的现金。
futures_margin = .... 是计算应达到的期货保证金价值M,只有达到这个值,才能达到对冲的目的,前文有公式:
# 调整股票仓位,在 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)
'''
for stock in g.in_position_stocks:
order_target_value(stock, stock_value/len(g.in_position_stocks), pindex=0)
for stock in g.in_position_stocks:
order_target_value(stock, stock_value/len(g.in_position_stocks), pindex=0)
'''
per_value = stock_value/len(g.in_position_stocks)
over_weight_list = [stock for stock in context.subportfolios[0].long_positions if \
context.subportfolios[0].long_positions[stock].value > per_value]
under_weight_list = [stock for stock in g.in_position_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)
上面这段,帖子里以及解释过了。虽然是两条重复的语句,但作用等同于我下面写的5条语句。就是等权调仓。
# 获取下月连续合约 string
current_future = get_next_month_future(context,'IF')
# 如果下月合约和原本持仓的期货不一样
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
# 获取沪深300价格
index_price = attribute_history('000300.XSHG',1, '1d', 'close').close.iloc[0]
# 计算并调整需要的空单仓位
order_target(current_future, int(futures_margin/(index_price*300*g.futures_margin_rate)), side='short', pindex=1)
按照前文计算出的应达到的保证金水平M,开空仓!
期指空单标的市值F的计算公式:
$$ F = N*indexPrice*300 $$其中:N为空单手数
开出这些空单需要保证金:
$$ M = mF = m*N*indexPrice*300 $$所以:
$$ N = \frac{M}{indexPrice*300*m} $$rebalance函数写得很精彩!¶