社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

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

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

  分析员,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
 
703 次点击