Py学习  »  Python

【中金·可转债】 模糊的股债性边界:认定、学习模型与Python实践

中金固定收益研究 • 1 年前 • 230 次点击  


模糊的股债性边界


如何评价转债的股债性,这本质上是一个“结果论”的问题:这只转债是展现了不负所望的进攻性,还是拿回了债券的稳健性。其实评价起来并不难,乃至历史上我们采用了非常简易的方法,即基于平底价比值的标准:大于1.2的我们习惯上认为是股性品种,低于0.8的则归为债性。现在似乎不能这样做了,我们看到了许多问题:


1. 由于估值夸张的分化水平,导致同等平价的转债,在溢价率上差异很大。这样的逻辑问题在历史上不是没有,但当时的估值分化,并没有使这个问题变成现实问题;


2. 正股、转债个体属性的差异大,导致在“该展现股性时”,某些品种却稳健了。而有的品种即便身背30%的溢价率,仍能因为上市不久、正股弹性较大等原因,完成了“股性”的任务;


3. 债性的问题则类似,现今大量平价很低的品种,也保持着大比例的债底溢价率。至少,我们不能认为一个YTM为-10%的品种,是“债性”的。


首先,我们先完成“刻画”的任务。先考虑比较简单的“债性”,我们并不计划去计算某只转债与纯债指数的相关性或者beta——由于交易活跃,且随时受到股市情绪影响,即便再偏债的转债,与债券指数之间也难以找到稳定的对应关系。但投资者无非想用这类品种获得稳定性——也就是接近债券的低波动、低回撤。而同时,我们知道“满分”应该是怎样的,大体上债底溢价率接近0且无信用疑虑的大品种,我们也可以默认130元以上的品种与“债性”并无关系——130元是多数赎回条款的触发线。我们可以用相对于债券指数的波动和回撤作为一个品种是否债性的标准(以下数据均为相对债券指数的超额波动及回撤,时间尺度默认为未来2个月):


1. 波动方面:年化39%及以上为0分,0.5%以内为100% —— 前者是130元以上品种的历史中位数水平,后者为纯债性品种的水平;


2. 最大回撤方面:15.5%以上为0分(130元以上转债的中位数水平),0%则是满分。

最终我们取这两部分的均值,得到一个转债是否够“债性”的描述。


图:债性得分示意图

资料来源:Wind,中金公司研究部


类似地,股性则在衡量“有行情”的情况下,个券把握机会的能力。 因此波动性、Beta水平应该是此时所关心的。我们用类似的方式,以几乎无溢价品种的水平为参考,给出以下认定:1、Beta值达到0.8以上为满分,0.2以下为0分 —— 前者是几乎无溢价率品种的中位数水平,后者是绝对债性品种的中位数水平;2、波动率达到37%以上为满分,10%以下为0分。


按照这样标准分类,好处在于更重实质——我们认为可以把中信、国君这样的转债划入债性转债,但平价同样在70元上下,溢价率也很高的蓝盾、垒知在这种分类下,将是接近0的“债性”,而非给予了相近的平价。再如溢价率只有10%、平价在百元附近的大秦转债,按上述标准则被划入“债性”——虽然溢价率不高,但正股本身甚至也有一定债性。


图:应被认定为“债性”的大秦,和不属于“债性”蓝盾

资料来源:Wind,中金公司研究部


下图则是全体样本的“股性分”和“债性分”的分布情况。显然存在负相关性,但也有很多品种似乎二者相加并不为1?—— 没错,一些实例包括一般被认为是“妖债”的品种,即没有明显的beta,也有着较高的回撤,这样的品种总分是不足1的。另有一些品种,既有一定的稳定性,但由于性价比高或者正股弹性更强等,也表现出一定股性,那么其总分溢出也并无逻辑问题——所谓“进可攻退可守”,大体如此。


图:转债股性分值与债性分值

资料来源:Wind,中金公司研究部


问题是显然的:这是事后的评价。beta、波动率、最大回撤都需要事实已经发生后才能衡量,我们需要在事前就能大体衡量的办法。正如在过去的十多年时间里,我们可以轻易地用平价或平价底价比值去做区分——如开篇所说,这在当下略显勉强——现在我们无非是考虑更细致的方式。


而就建模而言,问题已经并不难,因为我们有了前面的股性分与债性分作为“目标值Y”,剩下的工作只是打磨“X”。我们从“债性”开始,有一定相关性的因素包括:1、转股和债底溢价率;2、转债规模;3、剩余期限;4、正股波动率。显然这些因素可能并非线性地产生作用,但更显然我们没必要为了做性质区分,引入多层的学习网络(此前的估值预测模型)乃至自注意力模型(新一代定价模型原理)。因此我们仅让机器学习这些数据的利用方法——例如规模100亿元以上基本就注定了其“大盘”的属性,500亿与200亿元个券的债性差距,显然要小于200亿元与10亿元——然后衔接一个线性模型即可。


由于这个模型比较简单,我们介绍实现过程,实际上大多数非线性模型的做法大同小异。我们的原始数据形如下表,其中target为债性分,“Vol”为正股过去百周波动率,其他几列为转债指标,均为常见字段。


图:预测转债债性分值的原始数据示例

资料来源:Wind,中金公司研究部


由于要用到Pytorch(常见的如TensorFlow、Keras等代码相近),我们需要将数据表装载入张量(Tensor),并形成数据集。这里需要注意经常我们取原始数据后的dataframe并不是以浮点数存储数据的,因此用到astype(float)进行转换。以及,为了简单地形成训练集和测试集,我们需要用shuffle来打乱数据顺序。


图:预测数据处理前准备代码示意

import torch
import torch.nn as nn
import torch.utils.data as Data
import random

def frame2tensor(df):
    
    columns = ["ConvPrem", "StrbPrem", "Outstanding", "Ptm", "Vol"]
    tX = torch.tensor(df[columns].astype(float).values, dtype=torch.float32)
    tY = torch.tensor(df["target"].astype(float).values, dtype=torch.float32)
    return tX, tY

def tensor2iter(tX, tY):
    return Data.DataLoader(Data.TensorDataset(tX, tY), batch_size=30, shuffle=True)

tX, tY = frame2tensor(df) # df为上述原始数据
lst = list(range(tX.shape[0]))
train_iter = tensor2iter(tX[lst[:5500]], tY[lst[:5500]])
test_iter = tensor2iter(tX[lst[5500:]], tY[lst[5500:]])

资料来源:Wind,中金公司研究部


构造模型网络十分简单,这里我们更简单的直接将5个因素映射为5个代理变量,再经过激活函数(我们选择了上有顶、下有底的ReLu6),最终形成预测。下面的train_net则是训练这一网络的全过程,这里虽然不输出net,但在迭代训练的过程中,其参数慢慢改变,形成科学的预测值。当然,模型训练好之后,我们再去调用则十分简单。因此实际使用中,我们不必经历上述复杂的过程,下面的predict函数即可快速实现预测。


图:债性预测的训练模型

class bondElastic (nn.Module):
    def __init__(self):
        super(bondElastic, self).__init__()
        self.net = nn.Sequential(nn.Linear(5,5),
                                nn.ReLU6(),
                                nn.Linear(5,1))
    def forward(self, X):
        return self.net(X).reshape(-1)
        
def train_net(net, theIter, testIter, epochs=50, lr=0.001, weight_decay=0.0005):
    测算评估 = pd.DataFrame(index=list(range(epochs)), columns=["训练集损失", "测试集损失"])
    updater = torch.optim.Adam(net.parameters(), lr, weight_decay=weight_decay)
    loss = nn.MSELoss()
    
    for i in range(epochs):
        
        total_loss, test_loss = 0.0, 0.0
        
        for X, y in theIter:
            
            l = loss(net(X), y).sum()
            
            updater.zero_grad()
            l.backward()
            updater.step()
            
            total_loss += l.detach().numpy()
        
        for  X, y in testIter:
            test_loss += loss(net(X), y).sum().detach().numpy()
        
        print(f"在第{i+1}次训练,总损失为{total_loss:.2f},测试损失{test_loss:.2f}")
        测算评估.loc[i] = [total_loss, test_loss]
        
    return 测算评估
    
def predict(codes, date, obj, net):
    
    dfX = pd.DataFrame(index=codes, columns=["ConvPrem", "StrbPrem", "Outstanding", "Ptm", "Vol"])
    for field in dfX.columns[:4]:
        dfX.loc[codes, field] = obj.DB[field].loc[date, codes]
    
    dfX["Outstanding"] /= 100000000.0
    
    dfX["Vol"] = st.factor(codes, "annualstdevr_100w", date)
    X = torch.tensor(dfX.astype(float).values, dtype=torch.float32)
    return pd.Series(net(X).detach().numpy(), index=codes)
    
netTest = bondElastic()
测算评估 = train_net(netTest, train_iter, test_iter)

资料来源:Wind,中金公司研究部


下图为测试集与训练集的误差情况,可见由于数据量充足,且数据质量高,仅仅20次迭代便可达到不错的效果。


图:债性训练模型的效果情况

资料来源:Wind,中金公司研究部


而股性的预测则如出一辙,这里不再重复。下图是我们对2022年11月初(此后已经有了2个月的数据积累)个券进行的股债性预测,其整体的形态与前文“股性分与债性分”近似。


图:当前转债股债性的预测情况

资料来源:Wind,中金公司研究部


写在最后:为何此时需要探讨这个问题,甚至不惜提高一些复杂度?除了市场本身容量的改变以外,我们也要理解的是,市场估值在此前经历了较大幅度的回升 —— 虽然暂且还不形成直接的压力,我们也要明确,当下需要靠股性来找到收获,债性乃至连债性也称不上的品种,此时买入或继续持有的意义并不大。因此,我们尤其需要能够定量地明确,哪些品种我们可以称之为股性,哪些不能。


图:分类型的转债表现情况

资料来源:Wind,中金公司研究部


文章来源

本文摘自:2023年2月10日已经发布的《模糊的股债性边际:认定、学习模型与Python实践》

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

罗 凡 分析员 SAC 执证编号:S0080522070003

李奎霖 联系人 SAC执业证书编号:S0080122070189

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


法律声明

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


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