Py学习  »  Python

【中金固收·可转债】退市风险模型与策略的Python实践

中金固定收益研究 • 11 月前 • 345 次点击  


中金研究

退市风险“意外”成为近期转债投资者中的"新话题"。此前市场经历过的"压力情形"还是2018年的辉丰转债,后来转债遭遇暂停上市(股票未退市或暂停,彼时转债与股票的上市要求不同),该转债历史最低价格为2018年8月14日创下的71.86元。但近期搜特、正邦转债的价格都低于了70元,背后的差别无需多言,投资者需要了解的可能是:如何更早地看到并避免这类情况。好在问题并不复杂,一支转债跟随正股退市的情形无非是:市场因素(股价低于1元、市值低于3亿元)或者财务因素(触发财务退市指标),下面我们先对这些情景分别建模。



市场因素刻画


实践上,对市场退市的可能性建模,便可抵御大部分风险。市场退市的建模我们曾在2年前(2021年《挑战:EasyBall可以更稳吗——转债退市风险测算与Python实践》)中进行过尝试。简而言之,我们就是通过股价波动范围来计算未来股价低于1元或者市值低于3亿元的。这里我们希望将模型做得更简单一些,也更便于投资者掌握。因此,我们不再计算耗时耗力的Hurst指数,而只是用当前股价、市值、波动率和剩余期限作为参数,计算退市概率,下面介绍具体实现。


首先是取得波动率的数值,考虑到很多转债正股上市未满2年,直接从数据终端取2年历史波动率会出现大量空缺。因此这里首先从自行取得过去2年周回报数据开始,计算股价波动率。


图表1:Python实现:波动率


def 转债与正股对照表(codes):
    _, df = w.wss(codes, "underlyingcode", usedf=True)
    return df

def 正股周回报(stocks, start, end):
    
    sql = f'''select s_info_windcode windcode,
    trade_dt, s_wq_pctchange
    from winddf.ashareweeklyyield a
    where a.trade_dt >= {pd.to_datetime(start).strftime("%Y%m%d")} and
    a.trade_dt <= {pd.to_datetime(end).strftime("%Y%m%d")} and
    a.s_info_windcode in ('{"','".join(stocks)}')
    
    order by a.trade_dt
    '''

    con = rs.login() # con为SQL登录变量,视不同机构的设置而定,如不具备,可以考虑用api代替
    dfWeekly = pd.read_sql(sql, con)
    con.close()
    
    return dfWeekly.pivot(index="TRADE_DT", columns="WINDCODE" , values="S_WQ_PCTCHANGE")


def 指定日期百周波动率(dfWeekly, date):
    srs = dfWeekly.loc[:date].tail(100).std() * np.sqrt(50)
    srs.name = '波动率'
    return srs


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



然后我们装载其他变量,即股价、市值、转债剩余期限。其中的obj为我们在《转债数据库规范与统计案例》中设计的集成变量,可以按照github版本配置即可。beDelisted函数可以根据上述内容计算退市概率,而在得到上述数据表后,我们在"市场退市概率"中,只需要对原表进行apply操作便可以批量产出市场退市概率数据。


图表2:补充数据与计算概率


def 装载股价市值(obj, dfUnderlying, date):
    date = pd.to_datetime(date).strftime("%Y%m%d")
    if not w.isconnected(): w.start()
    stocks = list(dfUnderlying["UNDERLYINGCODE"].unique())
    _, dfData = w.wss(stocks, "close,,mkt_cap_ard", f"tradedate={date}",
                      usedf=True)
    dfRet = dfUnderlying.merge(dfData, left_on="UNDERLYINGCODE", right_index=True)
    dfRet["剩余期限"] = obj.Ptm.loc[date, dfUnderlying.index]
    dfRet["MKT_CAP_ARD"] /= 100000000.0
    return dfRet

def beDelisted(s, mv, vol, t):
    p1 = stats.norm.cdf((np.log(1) - np.log(s)) / (vol * (t ** 0.5)))
    p2 = stats.norm.cdf((np.log(3 ) - np.log(mv)) / (vol * (t ** 0.5)))
    
    return max((p1,p2))

def 市场退市概率(dfRet):
    dfRet["市场退市概率"] = dfRet.apply(lambda x: beDelisted(x["CLOSE"], x["MKT_CAP_ARD"],
                                                       x["波动率"] / 100.0,
                                                       np.min([x["剩余期限"],2])), axis=1)
    return dfRet



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



我们将上述操作拼装到下列函数,便可计算任意指定日期的市场违约概率,如下。



图表3:市场因素退市概率


def generateDelistedProb(obj, date):
    # 转债代码与正股对应表
    dfUnderlying = 转债与正股对照表(obj.selByAmt(date))
    stocks = list(dfUnderlying["UNDERLYINGCODE"].unique())
    # 取开始时点,计算波动率
    start = pd.to_datetime(date) - dt.timedelta(days=730)
    dfWeekly = 正股周回报(stocks, start, date)
    dfWeekly.index = pd.to_datetime(dfWeekly.index)
        
    srsVol = 指定日期百周波动率(dfWeekly, date)
    dfUnderlying = dfUnderlying.merge(srs, left_on="UNDERLYINGCODE", right_index=True)
    
    # 加载其他数据
    dfUnderlying = 装载股价市值(obj, dfUnderlying, date)
    
    return 市场退市概率(dfUnderlying)


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





财务因素刻画


近期案例中,也有一些品种的风险可以理解为由财务风险引发的。但对于转债投资者而言,难点在于取舍:1、一方面财务数据、指标众多,如果逐个查看再"综合分析",不仅效率低,一般也不易得到一个定量的结果;2、至少历史上,低评级(一般对应高的信用风险)转债的超额回报明显,即便考虑到近期案例,极端的风险概率毕竟很小。

因此,我们致力于一个简易、快速且答案明确的方法。首先尽可能控制决策成本(判断的过程),也让投资者在风险与收益的取舍中,有一个定量的数值进行参考。我们选取了过去3年有交易的、发行人为上市公司的公司债隐含评级作为目标变量y进行训练,而对于"X",一方面我们参考了评级机构的普遍做法(本质差异并不大),另一方面考虑数据的便捷、可靠,最终选取了:


1、"净债务比EBITDA"、"财务费用比EBITDA"、"总债务比总资本"来表征债务负担;


2、"EBITDA利润率"和"ROA"来表征盈利性;


3、"速动比率"和 "现金短期债务比"来表征流动性;


4、以及用"对数营收规模"和"对数总资产"表征规模。



图表4:财务数据获取代码


def _financialData(codes, rptDate):
    if not w.isconnected(): w.start()
    
    cols = ["净债务", "EBITDA", "财务费用","货币资金", "所有者权益",
            "营业收入", "ROA", "速动比率", "现金短期债务比", "总资产"]
    
    _, df = w.wss(codes, 'netdebt,ebitda2_ttm,finaexpense_ttm2,monetary_cap,tot_equity,\
                  or_ttm2,roa2_ttm2,quick,cashtostdebt,tot_assets'
,
                  f"unit=1;rptDate={rptDate};rptType=1", usedf=True)

    df.columns = cols
    df.dropna(how="any", inplace=True)
    
    df["净债务比EBITDA"] = df["净债务"] / df["EBITDA"]
    df["财务费用比EBITDA" ] = df["财务费用"] / df["EBITDA"]
    df["总债务比总资本"] = (df["净债务"] + df["货币资金"]) / df["所有者权益"]
    df["EBITDA利润率"] = df["EBITDA"] / df["营业收入"]
    df["对数营收规模"] = df["营业收入"].apply(lambda x: np.log(x / 100000000.0))
    df["对数总资产"] = df["总资产"].apply(lambda x: np.log(x / 100000000.0))
    
    colsRet = ["净债务比EBITDA", "财务费用比EBITDA", "总债务比总资本", "EBITDA利润率",
               "速动比率", "现金短期债务比",
               "对数营收规模", "对数总资产", "ROA"]
    
    return df[colsRet]


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



我们选择随机森林作为学习模型。我们没有考虑使用比较复杂的例如transformer或我们此前用过的Attention机制来进行训练,毕竟相对而言这一任务比较简单,数据量也并没有非常大。考虑到简易性和过拟合风险,我们使用在这个数据体量下表现较好的随机森林法来进行拟合学习。在已经清楚X和y的情况下,这个过程很容易,如下。注意,为控制过拟合,我们选择了最小枝叶为4的设定,投资者亦可尝试其他。



图表5:模型训练代码


from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor

def trainTree(X, y):
    scaler = StandardScaler()
    scaler.fit(X)
    X_scaled = scaler.transform(X)
    
    X_train, X_test, y_train, y_test = train_test_split(X_scaled, y,\
                                                        test_size=0.15, random_state=0)
    regr = RandomForestRegressor(min_samples_leaf=4)
    regr.fit(X_train, y_train)
    print('Training score:', regr.score(X_train, y_train))
    print('Test score:', regr.score(X_test, y_test))
    
    
    return regr, scaler


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



最终我们看到模型学习的结果也比较理想,训练集和测试集的拟合优度均可达到75%以上。预测值与实际值之间也有明显而单调的关系。尤其对于仅仅需要提示少数个券风险的转债投资者而言,足以达到效果。


图表6:财务退市模型:预测值与实际值

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



因此,在得到训练后的随机森林后,我们利用joblib将模型保存,并用于后续预测,预测函数如下。其中,rptDate为最近一期季报的时间。



图表7:财务退市模型实践代码


def 财务风险预测(codes, rptDate):
    df = _financialData(codes, rptDate)
    regr = joblib.load('regr.pkl')
    scaler = joblib.load('scaler.pkl')
    
    x_scaled = scaler.transform(df.fillna(0).values)
    yh = regr.predict(x_scaled)
    return pd.Series(yh, index=codes)


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



对投资者来说,一个重要的问题在于“怎么用”前面的两个结果。我们的一个建议是,在使用低价、双低类策略时,对市场因素的退市概率采取更为严格限定。因为一方面几乎所有面临退市风险的品种都要进入这个范围。另一方面,转债毕竟是很多发行人几乎唯一的公开债务,只要市场因素方面制约不大,公司回旋的余地就大。而财务因素则可能成为边际上的催化剂,因此我们可以作为补充限制条件,但只关注其中风险确实较大的品种。下图为我们的一些测试,在低价(前30%)、高YTM(前30%)和EasyBall基础上加入退市风险剔除的结果,剔除标准为:市场因素退市率 > 0.5%或财务因素退市率 > 16%,并进行每两个月一次的轮动。



图表8:三类策略净值情况

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





我们如何看待近期的变化?


1、首先,退市并非新事物,这类问题的讨论早在两年前已经有过(见《挑战:EasyBall可以更稳吗——转债退市风险测算与Python实践》[3]),而即便是具体品种,也经历过长期的演化、风险警示公告也很充分。仍在其中的投资者往往有着不一样的目的或信息,因此我们很难认为近期陆续出现的样本会给市场带来"意外",或"增量信息"。


2、作为专业的转债投资者,更多的是注意不被过于相近的品种牵连即可,上述方法可以用来帮助排除。


3、但更重要的是反应不宜过激,我们此前没有更多地讨论也是从投资策略角度看,对投资效果帮助不大。上述方法确实能够帮助一些低价策略降低最大回撤、提升卡玛比率——但投资者可以再对比下图,这是在上图基础上加入120日动量因子的效果,对于降回撤等方面的提升,要明显得多。



图表9:加入动量后的三类策略净值情况

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



4、另一个层次的“不要过激”的原因,则来自融资层面,转债发行人的中小型标的较多,投资者易于接受且由于变通余地大,实际的极端风险也较小。因而实际对这类中小标的而言,转债的意义很大,甚至也是最后的工具。因此,投资端不给予这个本就不“新”的现象过激的反应、调整,也有利于市场,尤其中小企业保持正常融资功能。



Source

文章来源

本文摘自:2023年5月12日已经发布的《退市风险模型与策略的Python实践》

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

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



Legal Disclaimer

法律声明

特别提示

本公众号不是中国国际金融股份有限公司(下称“中金公司”)研究报告的发布平台。本公众号只是转发中金公司已发布研究报告的部分观点,订阅者若使用本公众号所载资料,有可能会因缺乏对完整报告的了解或缺乏相关的解读而对资料中的关键假设、评级、目标价等内容产生理解上的歧义。订阅者如使用本资料,须寻求专业投资顾问的指导及解读。

本公众号所载信息、意见不构成所述证券或金融工具买卖的出价或征价,评级、目标价、估值、盈利预测等分析判断亦不构成对具体证券或金融工具在具体价位、具体时点、具体市场表现的投资建议。该等信息、意见在任何时候均不构成对任何人的具有针对性的、指导具体投资的操作意见,订阅者应当对本公众号中的信息和意见进行评估,根据自身情况自主做出投资决策并自行承担投资风险。

中金公司对本公众号所载资料的准确性、可靠性、时效性及完整性不作任何明示或暗示的保证。对依据或者使用本公众号所载资料所造成的任何后果,中金公司及/或其关联人员均不承担任何形式的责任。

本公众号仅面向中金公司中国内地客户,任何不符合前述条件的订阅者,敬请订阅前自行评估接收订阅内容的适当性。订阅本公众号不构成任何合同或承诺的基础,中金公司不因任何单纯订阅本公众号的行为而将订阅人视为中金公司的客户。

一般声明

本公众号仅是转发中金公司已发布报告的部分观点,所载盈利预测、目标价格、评级、估值等观点的给予是基于一系列的假设和前提条件,订阅者只有在了解相关报告中的全部信息基础上,才可能对相关观点形成比较全面的认识。如欲了解完整观点,应参见中金研究网站(http://research.cicc.com)所载完整报告。

本资料较之中金公司正式发布的报告存在延时转发的情况,并有可能因报告发布日之后的情势或其他因素的变更而不再准确或失效。本资料所载意见、评估及预测仅为报告出具日的观点和判断。该等意见、评估及预测无需通知即可随时更改。证券或金融工具的价格或价值走势可能受各种因素影响,过往的表现不应作为日后表现的预示和担保。在不同时期,中金公司可能会发出与本资料所载意见、评估及预测不一致的研究报告。中金公司的销售人员、交易人员以及其他专业人士可能会依据不同假设和标准、采用不同的分析方法而口头或书面发表与本资料意见不一致的市场评论和/或交易观点。

在法律许可的情况下,中金公司可能与本资料中提及公司正在建立或争取建立业务关系或服务关系。因此,订阅者应当考虑到中金公司及/或其相关人员可能存在影响本资料观点客观性的潜在利益冲突。与本资料相关的披露信息请访http://research.cicc.com/disclosure_cn,亦可参见近期已发布的关于相关公司的具体研究报告。

本订阅号是由中金公司研究部建立并维护的官方订阅号。本订阅号中所有资料的版权均为中金公司所有,未经书面许可任何机构和个人不得以任何形式转发、转载、翻版、复制、刊登、发表、修改、仿制或引用本订阅号中的内容。





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