繁簡切換您正在訪問的是FX168財經網,本網站所提供的內容及信息均遵守中華人民共和國香港特別行政區當地法律法規。

FX168财经网>人物频道>帖子

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

作者/不做外汇索罗斯 2019-07-10 09:33 0 来源: FX168财经网人物频道

研究对象:

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

研究目的:

  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函数写得很精彩!¶

 
分享到:
举报财经168客户端下载

全部回复

0/140

投稿 您想发表你的观点和看法?

更多人气分析师

  • 张亦巧

    人气2192文章4145粉丝45

    暂无个人简介信息

  • 王启蒙现货黄金

    人气296文章3220粉丝8

    本人做分析师以来,并专注于贵金属投资市场,尤其是在现货黄金...

  • 指导老师

    人气1864文章4423粉丝52

    暂无个人简介信息

  • 李冉晴

    人气2320文章3821粉丝34

    李冉晴,专业现贷实盘分析师。

  • 梁孟梵

    人气2176文章3177粉丝39

    qq:2294906466 了解群指导添加微信mfmacd

  • 张迎妤

    人气1896文章3305粉丝34

    个人专注于行情技术分析,消息面解读剖析,给予您第一时间方向...

  • 金泰铬J

    人气2328文章3925粉丝51

    投资问答解咨询金泰铬V/信tgtg67即可获取每日的实时资讯、行情...

  • 金算盘

    人气2696文章7761粉丝125

    高级分析师,混过名校,厮杀于股市和期货、证券市场多年,专注...

  • 金帝财神

    人气4760文章8329粉丝119

    本文由资深分析师金帝财神微信:934295330,指导黄金,白银,...

FX168财经

FX168财经学院

FX168财经

FX168北美