Brinson分析简介

策略分享
标签: #<Tag:0x00007f4cd00b8bb8>

(feynman0825) #1

Brinson分析简介

导语:收益归因是一个比较基础、同时相当重要的策略分析工具,本教程旨在帮助大家利用BigQuant自带的Brinson进策略进行绩效归因分析。

分析框架

Brinson的框架可以用来分解投资组合的总收益。尽管计算上很简单,但理论上是有效的,已被各种养老金赞助人、顾问和投资管理人员成功地使用;目前,它被用来表示实际投资组合中的业绩贡献。绩效归因虽然不是新发现的理论,但仍然是一个不断发展的学科。早期关于这一主题的论文主要关注风险调整后的收益,提出了最初的框架,但很少关注多重资产绩效衡量。我们的任务是按照投资客户和经理的决策的重要性排序,然后衡量这些决策对实际计划绩效的整体重要性。

表1说明了分析投资组合收益的框架:

  • 象限Ⅰ表示基准收益(Benchmark Return)。在此,我们将根据其长期投资基准确定投资组合的基准收益率。
    一个计划的基准收益是所采用的投资基准的结果。投资基准确定长期资产配置计划(包括资产类别和标准权重),用于控制总体风险和满足投资组合目标。
    简而言之,基准确定整个计划的投资组合的标准。要计算策略基准收益率,我们需要:(1)预先说明所有资产类别的权重,以及(2)分配在每种资产类别上的被动(或基准)收益。
  • 象限II表示基准和择时的收益(Benchmark and Timing Return)。这里象限II的收益并不单表示了择时的收益,而是按照基准进行选股加以主动择时的综合收益。择时是指相对于基准,以提高收益和/或降低风险为目的,在资产类比的标准权重上战略地降低或提高它的权重,择时表现了相对于政策回报的增量回报。
  • 象限III表示基准和选股的收益(Benchmark and Security Selection Return)而产生的收益。同样地,这里象限Ⅲ的收益并不单表示了选股的收益,而是按照基准进行择时加以主动选股的综合收益。选股是在一个资产类别中进行主动投资选择,我们将其定义为投资组合的实际资产类别收益率(例如,普通股和债券部分的实际回报率)超过这些类别的被动基准收益率,并由标准的资产配置权重进行加权。
  • 象限IV表示该期间基金总额的实际收益(Actual Portfolio Return)。这是主动进行择时和选股的实际结果。

表2给出了计算这些象限值的方法:

表3根据四个象限的值,计算出择时(Timing)、股票选择(Security selection)和两种的交互效应(Other),三者共同构成了组合的超额收益:

案例展示

下面我们用一个实际的策略案例来说明如何使用brinson分析。

这个策略是一个比较简单的双均线策略,当短期均线上穿长期均线,出现金叉,则买入;当短期均线下穿长期均线,出现死叉,则卖出。
交易时间我们选择从15年初到17年底,共三年时间。为演示目的,我们没有选择全市场的股票,而是抽了一些股票,这样回测结果跑得快些。

核心代码如下所示:

def run_stretegy1(instruments,start_date,end_date):

    # 金叉死叉策略
    #     当短期均线上穿长期均线,出现金叉,买入
    #     当短期均线下穿长期均线,出现死叉,卖出
    #     不风控:29.8%  26.61%  25.54%

    ## 1. 主要参数

    def prepare(context):
        context.pre_days = 60
        context.date = context.start_date
        tmp = datetime.datetime.strptime(context.start_date,'%Y-%m-%d') - datetime.timedelta(days=context.pre_days )
        #context.start_date =  tmp.strftime('%Y-%m-%d')
        start_date =  tmp.strftime('%Y-%m-%d')
        df = D.history_data(context.instruments, start_date, context.end_date, ['close'])
        #计算指标历史数据
        def ma_calculate(df):
            short_mavg = pd.rolling_mean(df['close'], 5)
            long_mavg = pd.rolling_mean(df['close'], 50) 
            df['ma_signal'] = (short_mavg>long_mavg).astype(np.int)
            return df
        context.ma_signal = df.groupby('instrument').apply(ma_calculate)[['date','instrument','ma_signal']].pivot(index='date', columns='instrument', values='ma_signal')

    ## 2. 模型训练

    def initialize(context):
        context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))
        context.short_period = 5
        context.long_period = 50
        assert context.long_period <=  context.pre_days


    def handle_data(context, data):
        date = data.current_dt.strftime('%Y-%m-%d')
        if date <  context.date:
            return

        for instrument in context.instruments:
            equity = context.symbol(instrument)
            ma_signal = context.ma_signal.ix[date,instrument]

            if np.isnan(ma_signal):
                ma_signal = 0

            # 获取账户现金和持仓
            cash = context.portfolio.cash  
            cur_position = context.portfolio.positions[equity].amount  

            #  交易逻辑
            #if  (state_choosed is None or context.options['state_pred'].ix[date,'state']==state_choosed) and ma_signal==1 and cur_position == 0 and data.can_trade(equity):
            if ma_signal== 1 and cur_position == 0 and data.can_trade(equity):
                context.order_target_percent(equity, 1/20.)   
            if ma_signal==0 and cur_position > 0 and data.can_trade(equity):  
                context.order_target_percent(equity, 0) 
    
    m=M.trade.v4(
                    instruments=instruments,
                    start_date=start_date,
                    end_date=end_date,
                    prepare=prepare,
                    initialize=initialize,
                    handle_data=handle_data,
                    volume_limit=0,
                    order_price_field_buy='open',
                    order_price_field_sell='open',
                    capital_base=float("1.0e6"),
                    plot_charts=True,
                    auto_cancel_non_tradable_orders=True,
                    benchmark='000300.SHA',
                    m_deps = np.random.randn())
    return m
    
instruments = ["600000.SHA","600010.SHA","600015.SHA","600016.SHA",
                "600028.SHA","600029.SHA","600030.SHA","600036.SHA",
                "600048.SHA","600050.SHA","600089.SHA","600100.SHA",
                "600104.SHA","600109.SHA","600111.SHA","600150.SHA",
                "600196.SHA","600256.SHA","600332.SHA","600340.SHA",
                "600372.SHA","600406.SHA","600485.SHA","600518.SHA",
                "600519.SHA","600547.SHA","600583.SHA","600585.SHA",
                "600606.SHA","600637.SHA","600690.SHA","600703.SHA",
                "600795.SHA","600837.SHA","600887.SHA",
                "600893.SHA","600919.SHA","600958.SHA","600999.SHA",
                "601006.SHA","601088.SHA","601118.SHA","601166.SHA",
                "601169.SHA","601186.SHA","601198.SHA","601211.SHA",
                "601288.SHA","601318.SHA",
                "601328.SHA","601336.SHA","601377.SHA","601390.SHA",
                "601398.SHA","601601.SHA","601628.SHA","601668.SHA",
                "601669.SHA","601688.SHA","601727.SHA","601766.SHA",
                "601788.SHA","601800.SHA","601818.SHA","601857.SHA",
                "601901.SHA","601919.SHA","601985.SHA",
                "600018.SHA","601988.SHA","601989.SHA","601998.SHA"]
start_date = '2015-01-01'
end_date = '2017-12-31'
strategy1 = run_stretegy1(instruments,start_date,end_date)

调用brinson分析api

当运行完回测的时候,保存下M.trade.v4的返回对象,在案例里我们存在strategy1里。强调一下,现在brinson分析只支持在v4版本上。
然后我们调用brinson_analysis()方法,这个方法会计算brinson分析所需要的数据,应该要不了几分钟。

brinson = strategy1.brinson_analysis()

当brinson对象构建后,我们调用plot_return_path()方法,来获得收益归因的路径图,对应的是上述表2的结果:

brinson.plot_return_path()

其中,RETURN_I是基准收益,相对应的RETURN_IV是组合实际收益。

然后我们来看下最关键的收益贡献分析:

brinson.plot_periods_return_analysis()

图中每根柱子代表相应收益在时间上的累积贡献,不难从上图中发现,这个策略有正的择时贡献,由于我们的双均线策略本来就是择时策略,所以并不奇怪。然而我们的选股收益是负的,说明我们的策略并不具有选股能力,分析我们的样例策略,是一个固定的股票列表,比基准的数量还要少,这个结果也能解释。

如果我们还可以单独看超额收益,如下图,可以看到我们的样例策略还是获得了超额收益。

小结:分解影响投资组合表现的因素,有利于量化投资管理决策在投资组合表现中发挥的作用;明确投资政策和投资策略之间的区别和联系将有助于进一步阐明这两项活动在投资过程中的作用。简单、准确、完整和可衡量的投资决策过程归因,将使我们进一步认识到投资活动中各部分的重要性,Brinson的理论在分析投资组合表现的决定因素上搭建起了一个简明而完整的框架。


(yangziriver) #2

复制了该策略,运行时出错


请老师帮助看一下。

克隆策略
In [1]:
def run_stretegy1(instruments,start_date,end_date):

    # 金叉死叉策略
    #     当短期均线上穿长期均线,出现金叉,买入
    #     当短期均线下穿长期均线,出现死叉,卖出
    #     不风控:29.8%  26.61%  25.54%

    ## 1. 主要参数

    def prepare(context):
        context.pre_days = 60
        context.date = context.start_date
        tmp = datetime.datetime.strptime(context.start_date,'%Y-%m-%d') - datetime.timedelta(days=context.pre_days )
        #context.start_date =  tmp.strftime('%Y-%m-%d')
        start_date =  tmp.strftime('%Y-%m-%d')
        df = D.history_data(context.instruments, start_date, context.end_date, ['close'])
        #计算指标历史数据
        def ma_calculate(df):
            short_mavg = pd.rolling_mean(df['close'], 5)
            long_mavg = pd.rolling_mean(df['close'], 50) 
            df['ma_signal'] = (short_mavg>long_mavg).astype(np.int)
            return df
        context.ma_signal = df.groupby('instrument').apply(ma_calculate)[['date','instrument','ma_signal']].pivot(index='date', columns='instrument', values='ma_signal')

    ## 2. 模型训练

    def initialize(context):
        context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))
        context.short_period = 5
        context.long_period = 50
        assert context.long_period <=  context.pre_days


    def handle_data(context, data):
        date = data.current_dt.strftime('%Y-%m-%d')
        if date <  context.date:
            return

        for instrument in context.instruments:
            equity = context.symbol(instrument)
            ma_signal = context.ma_signal.ix[date,instrument]

            if np.isnan(ma_signal):
                ma_signal = 0

            # 获取账户现金和持仓
            cash = context.portfolio.cash  
            cur_position = context.portfolio.positions[equity].amount  

            #  交易逻辑
            #if  (state_choosed is None or context.options['state_pred'].ix[date,'state']==state_choosed) and ma_signal==1 and cur_position == 0 and data.can_trade(equity):
            if ma_signal== 1 and cur_position == 0 and data.can_trade(equity):
                context.order_target_percent(equity, 1/20.)   
            if ma_signal==0 and cur_position > 0 and data.can_trade(equity):  
                context.order_target_percent(equity, 0) 
    
    m=M.trade.v4(
                    instruments=instruments,
                    start_date=start_date,
                    end_date=end_date,
                    prepare=prepare,
                    initialize=initialize,
                    handle_data=handle_data,
                    volume_limit=0,
                    order_price_field_buy='open',
                    order_price_field_sell='open',
                    capital_base=float("1.0e6"),
                    plot_charts=True,
                    auto_cancel_non_tradable_orders=True,
                    benchmark='000300.SHA',
                    m_deps = np.random.randn())
    return m
    
instruments = ["600000.SHA","600010.SHA","600015.SHA","600016.SHA",
                "600028.SHA","600029.SHA","600030.SHA","600036.SHA",
                "600048.SHA","600050.SHA","600089.SHA","600100.SHA",
                "600104.SHA","600109.SHA","600111.SHA","600150.SHA",
                "600196.SHA","600256.SHA","600332.SHA","600340.SHA",
                "600372.SHA","600406.SHA","600485.SHA","600518.SHA",
                "600519.SHA","600547.SHA","600583.SHA","600585.SHA",
                "600606.SHA","600637.SHA","600690.SHA","600703.SHA",
                "600795.SHA","600837.SHA","600887.SHA",
                "600893.SHA","600919.SHA","600958.SHA","600999.SHA",
                "601006.SHA","601088.SHA","601118.SHA","601166.SHA",
                "601169.SHA","601186.SHA","601198.SHA","601211.SHA",
                "601288.SHA","601318.SHA",
                "601328.SHA","601336.SHA","601377.SHA","601390.SHA",
                "601398.SHA","601601.SHA","601628.SHA","601668.SHA",
                "601669.SHA","601688.SHA","601727.SHA","601766.SHA",
                "601788.SHA","601800.SHA","601818.SHA","601857.SHA",
                "601901.SHA","601919.SHA","601985.SHA",
                "600018.SHA","601988.SHA","601989.SHA","601998.SHA"]
start_date = '2015-01-01'
end_date = '2017-12-31'
strategy1 = run_stretegy1(instruments,start_date,end_date)
  • 收益率36.11%
  • 年化收益率11.2%
  • 基准收益率14.07%
  • 阿尔法0.07
  • 贝塔0.58
  • 夏普比率0.47
  • 胜率0.34
  • 盈亏比2.01
  • 收益波动率21.0%
  • 信息比率0.02
  • 最大回撤43.95%
bigcharts-data-start/{"__type":"tabs","__id":"bigchart-f92dc4d1135d41d79f7eb9bee0352cce"}/bigcharts-data-end
In [2]:
brinson = strategy1.brinson_analysis()
In [3]:
brinson.plot_return_path()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-3-1c977fb445f6> in <module>()
----> 1 brinson.plot_return_path()

AttributeError: 'NoneType' object has no attribute 'plot_return_path'
In [ ]:
brinson.plot_periods_return_analysis()

(iQuant) #3

好的,我们来看一下。


(达达) #4

这个功能已经移到绩效归因模块,原接口不支持了。


(yangziriver) #5

好的,谢谢!我试试绩效归因模块。