请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  数理科学 帖子:3352830 新帖:30

“【量化课堂】股指期货对冲策略”之学习笔记

不做外汇索罗斯发表于:7 月 10 日 09:33回复(1)

研究对象:

【量化课堂】股指期货对冲策略

研究目的:

  1. 寻求一套通用的对冲例程,以期望用于各种资产组合。
  2. 学习大神编程经验,提高自己的python语言水平。

研究过程:

一、重写原版

  1. 发现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')
    
  2. 重写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怎么来的?有什么用?请看下面的“研究”

  3. 重写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')
             ]
         )
    
  4. 重写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是无用功。

  5. 重写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查询语句找出想要的答案。
    修改时发现了一个问题,就是“资产负债率”按大到小排列?
    一般地,资产负债率越高,表明该企业已经陷入或即将陷入财务困境,这样的股票表现能好么?
    为忠实再现原作,先不改,保持降序。策略不是重点,重点是对冲效果。

  6. 修改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

先看看股票账户相关的两条语句:

  1. transfer_cash(0,1,........), 是将现在股票仓位subportfolios[0]超出合理值S的部分调出到期货账户1中,但可能调不出那么多,需要今天卖股票,现在能调出的额度,受限于股票账户可动用现金transferable_cash。

  2. stock_value = min(......), 可以动用的股票资金,是股票现持仓H和股票合理价值S中二者的最小值,即如果H > S,那么多出的部分不能动用,是应该调出的,如果H < S,应该调入,但现在还没有到位,只能是现在有多少钱就用多少钱。

再看看期货账户相关的两条语句:

  1. transfer_cash(1,0,........),是期货账户subportfolios[1]中的钱调往股票账户0中,股票账户现值subportfolios[0].total_value低于股票仓位合理值S了,则说明期货账户钱多了,应将股票账户补到合理值S,但同样也受限于现在期货账户中可以动用的现金。

  2. futures_margin = .... 是计算应达到的期货保证金价值M,只有达到这个值,才能达到对冲的目的,前文有公式:

$$ M = mF = m \beta S $$
    # 调整股票仓位,在 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函数写得很精彩!¶

 

全部回复

0/140

量化课程

    移动端课程