Py学习  »  Python

【中金固收·可转债】至少有错位:“低估”策略优化与Python实现

中金固定收益研究 • 3 年前 • 582 次点击  

转债市场策略展望


244亿元——这是截止周四收盘,近十个交易日转债的成交额,也是今年来首次跌破250亿元。2020年3月之后,只有国庆节前和5月下旬与这个水平接近,而那也都是市场交易热情比较冷淡的时间段。与此同时,转债定价体系也进入了一个熟悉的节奏:分析师覆盖度超过市值、平价,成为最具决定性的因子——即有无较多分析师覆盖决定了转债是否会享有高溢价。



图表:转债十日平均成交额(剔除"妖债")

资料来源:万得资讯,中金公司研究部



问题就在,分析师覆盖度是历史上对转债后续走势,较为无关紧要的因子(可见《转债策略库及测试》)—— 这里并不意味着一定要买入低覆盖度的券,我们的意思是,结合成交量来看,转债市场上的投资者注意力已经比较涣散,定价系统处于非全负荷工作的状态,这就意味着相对低估值的做法应有回报。


有意思的是,近期也有一些投资者询问,到底“低估”策略应该怎样去做——毕竟已经经历过去年年底,那段真正“便宜没好券”的行情。我们的一些理解:


1、低估不等于低价。即便考虑去年那种情况,低估也总比高估强,参考下图。测算也支持的结论是,低估是偶尔出状况,而高估是一直在跑输——要考虑的只是补齐短板,比如加入趋势、退市风险或者成长性等方面的考虑。其中一个“特效”的方法,就是加入对高退市概率券的剔除,这也是我们写《转债退市风险测算与Python实现》的意义所在;



“双高”测验:价格与溢价率的综合排名

资料来源:万得资讯,中金公司研究部,数据摘自上周周报



2、我们对简单方法已经做过很多测算,包括双分位数法、双排名法和隐含波动率法。其中:1)双分位数法即选择价格和溢价率都在全市场某个分位数或某个值以下的品种——这也是两年前我们提出EasyBall时的做法;2)双排名法即对转债的价格和溢价率分别从小到大排名,再加总得到总排名,最后选择“总排名”靠前的;3)隐含波动率更为简单,选择隐含波动率较低的一部分转债即可。


这里我们不再重复一些过程和测算,投资者可见去年的《转债策略库及测试》以及更早的《转债也能做多因子吗?——附简易Python框架》结论就是:考虑换仓、风格分布和参数敏感性等,双排名法是最为稳健、效果也最好的方法,这也是我们自己后来更多高阶策略的基础。



简易“低估值”方法比较(单位为%)

资料来源:万得资讯,中金公司研究部,数据摘自2020年《转债策略库及测试》报告



我们当然有一些“高门槛”的方法,在此也做一个介绍。首先,此前几个简单方法都面临一个问题是,如果一味地去选“低估值”,就忽略了不同类型个券的禀赋问题:比如有时会更多地选小票,比如有时会更多地选正股趋势比较差的券——选了“低估值”的同时,也无意中给了组合一些风格偏向,这也是此前出问题的主因。实际上,我们不妨承认,不同类型的转债估值本应不同,如果我们去选“同类”转债中定价偏低的,就避免了上面这个问题。


例如当下,我们可以把转债分成三类:1)分析师覆盖度大于等于6的——6家以上给出买入或增持建议;2)分析师覆盖度小于6且正股过去60日下跌的;3)分析师覆盖度小于6且正股过去60日上涨的。在“平价——溢价率”平面上,上述三组会如下图分布。



转债分组平价、溢价率

资料来源:万得资讯,中金公司研究部



于是,理论上我们可以对上述三组分别拟合平价、溢价率关系曲线(以溢价率为y,以平价为x,y = a + bx + c/x + d x^2),然后选择溢价率在该曲线下方的,就可以实现前述目标——选同类中被低估的,而非估值最低的。


但是,分组标准并非始终如一,这样做也要牺牲掉效率。这里我们要借用更早前在《新券定位:方法、边际变化与python实现》(2019年11月)中的方法:对每一个个券,按照其关键属性——例如市值、分析师覆盖、成长性、技术指标——寻找其在转债中的“最近邻”,即与其最相近的若干个券。然后用“最近邻”的平价、溢价率拟合一个曲线,并以此计算该个券高估、低估的程度z。最后,我们选择z值较低的若干品种,形成策略。因此,整个实现过程多基于《新券定位:方法、边际变化与python实现》中介绍过的方法,只是当时的方法仅针对个券,此处我们要计算所有个券及其z值,因此我们需要对一些函数进行改造,如下面函数所示。当然也都非常简单,没有特别需要提示注意的地方。



前述方法需要改造的函数


def neighborsCbGroups(codes, date):
    # 寻找紧邻的函数,方法类似Knn
    df1 = getIssueAndConvValueGroup(codes, date) #取转债发行额、日期与平价
    df2 = getStockFactor(codes, date) # 取正股因子,都请见《新券定价》
    
    dfCampareData = pd.concat([df1,df2],axis=1)
    dfCampareData[u'规模'] = dfCampareData[u'规模'].apply(lambda x: pd.np.log(x))
    
    dfCampareData.dropna(inplace=True)
    dfZ = (dfCampareData - dfCampareData.mean()) / dfCampareData.std()
    
    dictRet = {}
    for newSec in codes:

        srsDistance = (dfZ - dfZ.loc[newSec]).apply(lambda x: x ** 2).sum(axis=1)
        
        k = int(min([max([6, pd.np.floor(len(codes) / 10.0)]), pd.np.floor(len(codes) / 2.0), 18]))
        lstRet = list(srsDistance.sort_values().index[1:k])
        
        dictRet[newSec] = {"lstRet" : lstRet, "df": dfCampareData.loc[lstRet]}
    
    return dictRet


def olsForGroup(dfKnnData, conv):
    # 拟合曲线并返回定价的函数,需要近邻列表和平价值
    dfKnnData['ones'] = 1.0
    dfKnnData['1/conv'] = 1.0 / dfKnnData["conv"]
    
    lr = LinearRegression(fit_intercept=False)
    
    lr.fit(dfKnnData.loc[:,['ones','conv','1/conv']].values, dfKnnData['prem'].values)
    coef = lr.coef_
    
    return conv * (1 + (coef[0] + coef[1] * conv + coef[2] / conv) / 100.0), lr.coef_

资料来源:万得资讯,中金公司研究部



随后,我们便可将前述操作封装入class对象以便调用。其中,groupValuation.ret中的“diff”即为估价价差,我们选择较小者。



Class对象:估值拟合与价差


class groupValuation(object):
    def __init__(self, codes, date):
         # 输入样本券、日期即可
        self.codes, self.date= codes, date
        self.knn() # 寻找近邻
        self .getXY() # 取平价和溢价率数据,见《新券定价》
        self.valuation() # 计算估价,diff列即为差价,策略应取diff较小的达到低估值效果
    
    def knn(self):
        self.dictNeighbor = neighborsCbGroups(self.codes, self.date)
    
    def getXY(self):
        self.dfXY = getConvAndPremium(self.codes, self.date)

    def valuation(self):
        dfRet = pd.DataFrame(index=self.codes, columns=["v", "close","diff"])
        
        for code in self.codes:
            lstKnn = self.dictNeighbor[code]["lstRet"]
            dfKnnData = self.dfXY.loc[lstKnn]
            conv = self.dfXY.loc[code, "conv"]
            v, coef = olsForGroup(dfKnnData, conv)
            dfRet.loc[code, "v"] = v
            dfRet.loc[code, "close"] = self.dfXY.loc[code, "conv"] * (1 + self.dfXY.loc[code, "prem"]/ 100.0)
        dfRet["diff"] = dfRet["close"] - dfRet["v"]
        dfRet.dropna(how="any", inplace=True)
        
        self.ret = dfRet

资料来源:万得资讯,中金公司研究部



而相比于“双排名法”,这种考虑分组定价的优势在于单调性和关键风格基本中性。以下为基于分位数的5组测算,可见按照这种方法计算,长期来看分位数越低、效果越好,而双排名法则是50%分位数以下没有档次差——不过有时候这也是个优点。同时,虽然“0~20%”组在去年年底依然遭遇一些挑战,但相对而言可以接受。



分组定价法:5组测算(分位数越低越代表低估)

资料来源:万得资讯,中金公司研究部



当然,这种方法也只是作为一个基础策略,其与其他强化策略也有较强的相容性,例如下图为基于技术指标进行的趋势强化的效果。但策略永远有效率和产量的矛盾,强化的方式也不是我们今天讨论的重点,此处点到为止。



低估 + 强化策略

资料来源:万得资讯,中金公司研究部



最后,我们对当前行情的看法与此前没有太大变化。往年此时也是转债的弱势月份,投资者少有在3~5月赚钱的经历,而股市处于磨底期,最重要的反而是保持一个好的心态以迎接下一阶段的机会。但要注意的是,无论从股市还是转债的角度看,市场情绪都有出清的势头——除了开头提到的转债成交量以外,万得全A指数的单日成交额也在周四进入了我们的目标范围:我们希望看到十日平均成交额缩至6500亿元以内,而周四已经跌至6485亿元。因此,结合转债已经不高的估值,我们倾向于认为现在虽然尚未到达底部,但已经可以开始更为积极地面对。至于底部的时点,观察大于预测,经验上可能会在2~3周后出现。而至少从技术上理解,当前市场的风险点在于前期抱团品种尚未真正完成调整,其他品种则可能在此过程中受累,因此结构上的战略是明确的,绕开前期抱团品种,这也是我们希望在此时多介绍一下低估值策略的原因



上证指数:典型磨底状态

资料来源:万得资讯,中金公司研究部



基金重仓指数

资料来源:万得资讯,中金公司研究部




市场一周概况


本周市场总体维持窄幅震荡,截止周四(4月15日)收盘,万得全A下跌1.37%,创业板指下跌1.29%,上证50下跌2.35%,中证1000下跌1.86%。两市合计成交2.73万亿元,日均约6825亿元,低于上周,主要指数波动率低位钝化。板块方面,本周行业轮动速度较快,电力设备、综合金融、汽车、有色板块表现靠前,消费者服务、交运、轻工及公用事业排名靠后,周五早盘煤炭、传媒及商贸零售表现抢眼。主题概念方面,白酒、数字货币、新冠检测、二胎政策概念涨幅较大。转债指数本周下跌0.91%,小康、中矿、联泰、蓝帆及飞鹿转债涨幅居前,金能、英科、华锋、金禾转债领跌。




一级市场跟踪


本周新公告5个转债预案,分别为苏利股份(10亿元)、红墙股份(5.2亿元)、耐普矿机(4亿元)、通威股份(120亿元)与元力股份(9亿元);首华燃气(20亿元)获受理;江丰电子(5.17亿元)过发审委;北部湾港(30亿元)、惠城环保(3.2亿元)与捷捷微电(11.95亿元)获核准;目前已核准发行个券27只,合计金额359.65亿元。已过会未核准个券10只,合计金额207.32亿元。


图表:拟发行转债、EB

资料来源:万得资讯,中金公司研究部,数据截止2021年4月15日



私募EB跟踪


本周无新增私募EB申请。淄博齐翔石油化工集团有限公司EB(30亿元)与广州雪松文化旅游投资有限公司EB(12亿元)获反馈。


图表:私募EB拟发行信息

资料来源:万得资讯,中金公司研究部,数据截止2021年4月15日





相关报告推荐

1、新券定位:方法、边际变化与python实现
2、转债策略库及测试
3、 转债退市风险测算与Python实现
4、转债“反指”大筛查
5、等的就是调整,兼论估值观测方法
6、关于转债正股的“体检



文章来源

本文摘自:2021年4月16日已经发布的《至少有错位:“低估”策略优化与Python实现》

杨 冰 SAC执业证书编号:S0080515120002;SFC CE Ref: BOM868

房 铎 SAC执业证书编号:S0080519110001

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



法律声明

向上滑动参见完整法律声明及二维码


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