Py学习  »  Python

【中金固收·可转债】转债基金择时、择券能力如何区分?—— 及Python实现方法 20190729

中金固定收益研究 • 4 年前 • 724 次点击  
作者

  分析员,SAC执业证书编号:S0080515120002   SFC CE Ref: BOM868

吴若磊联系人,SAC执业证书编号:S0080119030020

陈健恒分析员SAC执业证书编号S0080511030011 SFC CE Ref: BBM220


转债基金择时、择券能力如何区分

在转债周报《这半年,谁的择时精准,谁的择券有效》中,我们讨论了转债基金的择时、择券能力。我们在该周报也进行了简短的介绍,当然其中一个假设是没有多少投资者对这个业绩拆分的过程感兴趣。不过虽然与我们讨论程序实现方法的投资者比意料中多,我们在本专题中进行专门介绍。

基础模型实际很简单。我们采用了认可度比较高,又有着诸多其他优点(后面会详细说)的CL模型。基础框架如下:

Rp - Rf = alpha + beta1 * min(Rm - Rf, 0) + beta2 *max(Rm - Rf, 0) + e

几点解释:

1、实际这个式子没有比CAPM复杂很多,只是把Rm - Rf再拆分成下跌上涨两种状态,分别对应beta1beta2两个敏感系数。所以在这里,alpha 显著大于0,则可以认为是有择券效果,而在beta2 > 0的情况下,beta2 - beta1比较大,说明基金能在上涨和下跌阶段,体现出不同的beta值来。最理想的情况莫过于beta2很大,beta1接近0 —— 这就是一般情况下说的精准逃顶了;

2、但alphabeta 仍然不能完全被拆分开,比如有的品种就是拥有超大的Beta值,包括券商转债、某些情况下的周期转债(例如之前的三一、有色品种)以及溢价率很低的银行转债。再如,有的品种就是有很强的不对称性,例如诸多低估值品种,跌已无太多空间,而涨却也无溢价来压制弹性,最终以择券的形式,产出的择时的效果 —— 这也是模型无能为力的方面;

3Rf怎么选一直是个问题,国开债收益率、企业债收益率都是个办法。不过实际还是基金倾向于拿哪一类当,哪一类就更合适(历史上或许是城投,最近来看基金更愿意拿利率债)。所以有一个还算稳定的方法是:用不拿转债\股票的长债基金收益率 —— 当然用哪个都不是重点;

4、这始终是个归因模型,不是测仓位手段,所以无论beta1还是beta2都与仓位这个概念有差异。无论如何,这里讨论的是实际效果,或者有效仓位的概念,不必纠结于实际仓位是多少 —— 满仓特发和满仓山高EB效果自然是不一样的。

下面进入实现的流程。在确定基金研究范围的情况下,我们需要的其实只是以下这些数据:基金调整后净值、转债指数、长债基金指数。我们还是用class将这些数据的初始化、获取、处理和最终的计算封装,初始化部分如下。里面涉及到_fetchAdjNav_fetchCBIndex_fetchBondIndex这三个私用函数(在私用函数前加一个"_",以区别于公用函数)。以及,self.dfNav = self.dfNav.reindex(columns=codes)这句是利用pandas中的reindex来对表格的列进行重排,比较好理解。最后会得到.dfNav.dfIndex两个pandas下的DataFrame,分别是净值和转债、债基指数的表。

import pandas as pd 

from sklearn.linear_model import LinearRegression

 

class fundAttr(object):

   

    '''写文档是美德

    初始化输入为:codesstartend,字面意思   

    '''

   

    def __init__(self,codes,start,end):

       

       self.codes=codes

       self.start=start

       self.end=end

       

       self.dfNav=self._fetchAdjNav()

       self.dfNav=self.dfNav.reindex(columns=codes)

       

       self.dfIndex=pd.DataFrame(index=self.dfNav.index,columns=['CB','BOND'])

       

       self.dfIndex['CB']=self._fetchCBIndex()

       self.dfIndex['BOND']=self._fetchBondIndex()

下面是_fetchAdjNav_fetchCBIndex_fetchBondIndex这三个私用函数,比较好理解,不用解释。有一些做法则是习惯的问题,比如df.sort_index(inplace=True)这句不一定要加,如果习惯于在sql的最后一句加上"order bytradedate"的话。以及,其实如果做对外接口的话,没有必要做三个函数,而是一个就够了,这里面存在有待商榷的地方。

    def _fetchAdjNav(self, method='sql'):

       

            if method =='sql':

            #这是sql版本

                   sql ='''select a.f_info_windcode windcode,

                   a.price_datetradedate,

                   a.f_nav_adjustednav

           

            from winddf.chinamutualfundnava

           

            where

            a.f_info_windcodein({_codes}) and

            a.price_date>={_start} and

            a.price_date<={_end}

            '''.format(_codes= '"' + ','.join(self.codes) + '"',

            _start = self.start,

            _end = self.end)

           

            con =mylogin() # concxOracle的登录变量,一般输入服务器地址等信息

           

            df = pd.read_sql(sql, con)

           

            con.close()

           

            df = df.pivot(index='TRADEDATE', columns='WINDCODE', values='NAV')

            df.sort_index(inplace=True)

       

            elif method=='api':

            #这是万得pythonapi的版本

            if not w.isconnected(): w.start()

           

            wobj = w.wsd(','.join(self.codes),'adjnav', start, end)

            df = pd.DataFrame(wobj.Data.transpose(), index=wobj.Times.apply(lambda x:'/'. join([x.year, x.month, x.day]), columns=self.codes)

           

            w.close()

            else:

           

            raise ValueError u"method不对"

           

            return df


    def _fetchCBIndex(self, method='sql'):

       

            if method =='sql'       

             sql ='''select a.s_info_windcodewindcode,

            a.trade_dttradedate,

            a.s_dq_closeindexprice

           

            from winddf.aindexeodpricesa

            where a.s_info_windcode='000832.CSI' and

             a.trade_dt >={_start} and

            a.trade_dt <={_end}       

            '''.format(_start=self.start, _end=self.end)

           

            con =mylogin()       

            df = pd.read_sql(sql, con)       

             con.close()

           

            df = df.pivot(index='TRADEDATE', columns='WINDCODE', values='INDEXPRICE')

           

            df.sort_index(inplace=True)

            elif method=='api':

 

            #这是万得pythonapi的版本

             if not w.isconnected(): w.start()

           

            wobj = w.wsd("000832.CSI",'close', start, end)

            #这里面注意,api有时候在只有1code被输入时,.Data吐出来一个一维变量,用之前先试一下最好

            dfpd.DataFrame(wobj.Data.transpose(), index=wobj.Times.apply(lambda x:'/'.join([x. year, x.month, x.day]), columns=self.codes)

           

            w.close()

            else:           

            raise ValueError u"method不对"

           

            return df

   


    def _fetchBondIndex(self):

       

            if method =='sql' 

      

             sql ='''select a.s_info_windcode windcode,

            a.trade_dttradedate,

            a.s_dq_closeindexprice

           

            from winddf.AIndexWindIndustriesEODa

            where a.s_info_windcode='885008.WI' and

            a.trade_dt >={ _start} and

            a.trade_dt <={_end}       

            '''.format(_start=self.start, _end=self.end)

           

            con =mylogin()       

            df = pd.read_sql(sql, con)       

             con.close()

           

            df df.pivot(index='TRADEDATE'columns='WINDCODE'values='INDEXPRICE')

           

            df.sort_index(inplace=True)

            

            elif method == 'api':

   

            #这是万得pythonapi的版本

             if not w.isconnected(): w.start()

           

            wobj = w.wsd("885008.WI",'close', start, end)

            #这里面注意,api有时候在只有1code被输入时,.Data吐出来一个一维变量,用之前先试一下最好

            df pd.DataFrame(wobj.Data.transpose(), index=wobj.Times.apply(lambda x:'/'. join([x.year, x.month, x.day]), columns=self.codes)

           

            w.close()

            else:           

            raiseValueError u"method不对"

           

            return df  

然后是数据的计算,这一块反而是比较简单的,借助sklearnLinearRegression就行,而且效率很高。这里也充分体现了能不用for就不用的原则。最后需要的是sklearn拟合出来的coef_这个变量。

    def anal_CL(self):

       

            dfRet = pd.DataFrame(index=self.codes, columns=['alpha','beta1','beta2'])

       

           pctNav = self.dfNav.pct_change()       

           pctIndex = self.dfIndex.pct_change()


           for col in pctNav.columns:

            pctNav[col]-= pctIndex['BOND'] 

       

           pctNav.dropna(inplace=True)

       

            x = pctIndex['CB']- pctIndex['BOND']

       

           x1 =x.apply(lambda y:min([y,0]))

           x2 =x.apply(lambda y:max([y,0]))

       

           dfX = pd.DataFrame(index=x1.index)

       

           dfX['Ones']=1.0

           dfX['x1']= x1 *100.0

           dfX['x2']= x2 *100.0

       

           dfX.dropna(inplace=True)               

       

           lr =LinearRegression(fit_intercept=False)

       

           lr.fit(dfX.reindex(index=pctNav.index).values, pctNav.values *100.0)

           dfRet.iloc[:,:]= lr.coef_

       

           return dfRet

最后,还是要提醒投资者的是,模型给不同的人,用出来效果是不同的 —— 取决于理解程度。以下几点应当注意:

1、如前所述,alphabeta无法完全分离,尤其转债还存在不同品种股性完全不同的、以及Gamma大小差异很大的问题;

2、单看某一个数值总是容易出错,比如beta2 - beta1从数据上代表了择时能力,但也要结合仓位和基金规模大小来看。alpha也要注意结合beta2来评判,25 45%命中率,和560%命中率相比,还是前者更强一些;

3、最后,择时和择券,两者兼顾很难很难,往往一端强势,另一端不拖就已经是不错的管理,更多这个方面的评论请见转债周报。



本文所引为报告摘要部分内容,报告原文请见2019729中金固定收益研究发表的研究报告《中金公司*杨冰,吴若磊,陈健恒:简评*转债基金择时、择券能力如何区分? | —— 及Python实现方法


相关法律声明请参照:

http://www.cicc.com/portal/wechatdisclaimer_cn.xhtml





Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/36668
 
724 次点击