【中金固收·固收+】隐藏价值的角落:限售股AAP估值及Python实现方法(上)

【中金固收·固收+】隐藏价值的角落:限售股AAP估值及Python实现方法(上)
2019年12月01日 14:32 新浪财经-自媒体综合

感知中国经济的真实温度,见证逐梦时代的前行脚步。谁能代表2019年度商业最强驱动力?点击投票,评选你心中的“2019十大经济年度人物”。【我要投票

来源:中金固定收益研究

作者

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

程昱分析员,SAC执业证书编号:S0080517070005;SFC CE Ref:BON965

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

或许市场不够重视,但这可能也是隐藏价值的一个角落。配售和定增基金往往会持有一些流通受限的股票,而这些股票估值的方法在2017年经历了一次比较大的变化——根据《证券投资基金投资流通受限股票估值指引(试行)》,限售股适用AAP估值法(亚式看跌期权),取代此前近似摊余成本的方法。目前,主要是定增基金和配售基金涉及比较多这类问题。

实事求是地说,即使在以前的简易算法下,虽然公式简单,但数据处理存在一定难度,投资者对于当时(比如2016年)定增基金的净值也存在一定疑虑。而现在公式更为复杂了,对投资者来说黑箱化的程度也自然更高了。在以前的模式下,限售股(比如定增得来的股份)的估值为:V= min(当前价,成本价+ (当前价 - 成本价)* 已持有的时间 / 总锁定期),或者表示如下图。

而新的AAP模型估值法下,估值 = 市场价* (1 -折扣系数),折扣系数为按照亚式看跌期权计算出的期权价值(LoMD)。公式如下:

可以看出,想比之前近似摊余成本法的计算模式,这里复杂程度还是要高出不少的。其中,σ为该股票波动率,q为分红率,T为以年为单位的锁定时间,其他均为字面意思。实际上,上面公式在实现时基本没有难度,更多的难点在于数据的处理。

下面,我们来一步一步拆解这个LoMD。首先自然需要引入一些常见库:

import readSql as rs

import pandas as pd

from scipy import stats

from WindPy import w

import datetime as dt

其中的readSql为我们惯用的自编库,在此前的转债报告中也曾出现过,但为了不产生太多歧义,我们只用其中的几个函数即str2yyyymmdd、yyyymmdd2str:在日期的两种字符串之间互相转换,例如“2019/1/18”转成“20190118”及其逆运算。

下面我们先展示最后的主函数,然后逐个击破涉及到的中间函数。整个主函数如下所示。第一个参数dfHeld为pandas下的DataFrame型,其中保存的是基金所持限售股的信息(参考年报、半年报以及基金临时公告整理),示例格式如下图。而后面的start和end分别为计算期的起止时间,我们最后返回了在这个期间内,一组限售股的折扣系数,形式同样是DataFrame。

defdiscRatioTable(dfHeld, start,end):

   '''输入限售股table,输出这些券对应的LoMD

   start: 计算首日,yyyy/mm/dd

   end: 计算终日, yyyy/mm/dd'''

# 先计算这些股票的vol

   lstStocks = list(dfHeld.index)

   nDaysBefore, flag = backToTime(start, list(dfHeld[u'可流通日期'].apply(rs.yyyymmdd2str)))

if flag:

       dfAdjClose = priceData(lstStocks, nDaysBefore, end)

       dfLogPct = logPctData(dfAdjClose)

       dfLogPct.to_clipboard()

else:

print u'没有限售股,不必算了'

return None

   dfVol = pd.DataFrame(index=dfAdjClose.loc[start:end].index,columns=lstStocks)

for code in lstStocks:

for date in dfVol.index:

            days = _deletaTradingDate(date,rs.yyyymmdd2str(dfHeld.loc[code, u'可流通日期']))

            dfVol.loc[date, code] =calcVol(dfLogPct, code, date, days) if days > 0 else None

# 然后是q,股息率

   dfDiv = dividendYield(lstStocks, start, end)

# 最后计算discRatio表

   dfDiscRatio = pd.DataFrame(index=dfVol.index, columns=lstStocks)

for code in lstStocks:

       strEnd = rs.yyyymmdd2str(dfHeld.loc[code, u'可流通日期'])

for date in dfDiscRatio.index:

            t = deltaT(date, strEnd)

dfDiscRatio.loc[date,code] = aapDisc(dfDiv.loc[date, code], dfVol.loc[date, code], t)

return dfDiscRatio

观察整个函数结构,可以比较清晰地看到,整个过程分为三步

1、计算波动率表;

2、计算股息率表;

3、根据前两步的结果计算折扣系数并最终返回。

下面来计算波动率σ。根据《指引》,其计算方法为先确定计算时点距离股票可流通的到期日数(暂且记作n,但当n不足20时取20)。然后用计算日前n日股价的历史对数收益率计算波动率。因此,这里实际第一步是计算n(下方_deletaTradingDate函数),然后确定n个交易日之前是哪一天(下方backToTime函数)。两个函数中间,用tDaysBefore作为过渡。而由于我们每次对一个基金进行计算时,往往其所持限售股不止一个,因此我们将第二个参数类型预设为列表(list)。具体如下:

defbackToTime(now, lstDays):

   ‘’’now是计算时点,lstDays是解禁日列表’’’

   dtNow = dt.datetime.strptime(now, '%Y/%m/%d')

   dtMax = max([dt.datetime.strptime(t, '%Y/%m/%d') for t inlstDays])

   strMax = dt.datetime.strftime(dtMax, '%Y/%m/%d')

if dtMax > dtNow:

       n = _deletaTradingDate(now, strMax)

return tDaysBefore(now, n), 1

else:

return now, 0

def_deletaTradingDate(start,end):

   '''交易日差(以日为单位)'''

   obj = w.tdayscount(start, end)

if obj.ErrorCode == 0:       

return obj.Data[0][0]

else:

print "Connect ErrorCode",obj.ErrorCode

raise ValueError(u'算交易日出现错误')

deftDaysBefore(now, days):

   obj = w.tdaysoffset(-days, now)

return dt.datetime.strftime(obj.Data[0][0],'%Y/%m/%d')

在主函数中,我们用上述方法确定计算时间的起止点,确定好之后,就可以提取股价、计算对数收益率并据此计算波动率了。这里有一定难点是:如果股票在预定的计算区间内无成交(比如停牌,或者新股未上市),需要用对应行业的AMAC指数数据作为替代,详见下方logPctData函数。

def loadData(tickers, field, start,end, *others):   

   # 用万得API取数据的通用函数

   strTickers = ','.join(tickers)

   obj = w.wsd(strTickers, field, start, end, others)

   arrData = np.array(obj.Data).transpose()

   srsDate = [dt2str(d) for d in obj.Times]   

return pd.DataFrame(arrData,index = srsDate, columns = tickers)

defpriceData(lstStocks, start,end):

   '''取复权价

   lstStocks:股票列表

   start:yyyy/m/d格式的时间起点

   end:时间终点'''

   dfAdjClose = loadData(lstStocks, ‘close’, start, end, ‘Priceadj=B’)

return dfAdjClose

deflogPctData(dfAdjClose):

   dfLogPct = dfAdjClose.applymap(np.log) - dfAdjClose.shift(1).applymap(np.log)

   t = dfAdjClose.apply(lambda x: not(all(pd.notnull(x))))

lstNullCodes =list(set(t[t].index))

# 下面的内容是,如果存在异常数据,则用行业指数替代

if lstNullCodes:

       obj = w.wss(','.join(lstNullCodes), 'indexcode_AMAC', 'tradeDate={_end}'.format(_end=rs.str2yyyymmdd(dfAdjClose.index[-1])))

       dfStockVsIndex = pd.DataFrame(np.array(obj.Data).transpose(),index=obj.Codes, columns=['AMAC'])

       dfIndex = loadData(dfStockVsIndex[‘AMAC’].values.tolist(), ‘close’,dfAdjClose.index[0],dfAdjClose.index[-1])

       dfPctIndex = dfIndex.applymap(np.log) - dfIndex.shift(1).applymap(np.log)

for i in xrange(1,len(dfLogPct)):

            t = dfLogPct.iloc[i].isnull()

            lstNullInThisRow = list(t[t].index)

iflstNullInThisRow:               

for c inlstNullInThisRow:

                    idxID =dfStockVsIndex.loc[c, 'AMAC']

                    dfLogPct.iloc[i].loc[c] =dfPctIndex.iloc[i].loc[idxID]

return dfLogPct

这一步最简单的反而是最终计算波动率,如下函数,没有太多值得解释的地方。注意,下面的函数是计算单个股票、单个交易日的波动率,实际存在一个效率的问题(显然不如整个DataFrame直接去算,这也是我们将在实际使用时修正的内容,这个问题在最后算折扣率时也存在)。

defcalcVol(dfLogPct, ticker, now,days):

   '''dfLogPct:复权收益表

   ticker:代码

   now:yyyy/mm/dd

   days:自然数字,时间窗口'''   

   loc = dfLogPct.index.get_loc(now)

   start = max([0, loc-int(days)])

return dfLogPct.iloc[start:loc].loc[:, ticker].std() *np.sqrt(250)

然后是股息率。类似地,这里有一个问题是如果上市未满一年,股息率需要用行业指数做替代。不过相比于波动率,这里毕竟不涉及日期处理,整体要简单一些。

defdividendYield(lstStocks,start, end):

   obj = w.wss(','.join(list(set(lstStocks))), 'ipo_date')

   dfIPO_Date = pd.DataFrame(np.array(obj.Data).transpose(),index=lstStocks, columns=['ipo_date'])

   dfIPO_Date['OneYearAfter'] = dfIPO_Date['ipo_date'].apply(lambdax: x + dt.timedelta(365))

   obj = w.wss(','.join(list(set(lstStocks))), 'indexcode_AMAC','tradeDate={_end}'.format(_end=rs.str2yyyymmdd(end)))

   dfStockVsIndex = pd.DataFrame(np.array(obj.Data).transpose(),index=lstStocks, columns=['AMAC'])

   dfStockVsIndex.fillna('881001.WI', inplace=True)

   lstIndexCodes = list(set(dfStockVsIndex['AMAC'].values))

   dfStockDiv = loadData(lstStocks, ‘dividendyield2’, start, end)

   dfIndexDiv = loadData(lstIndexCodes, ‘dividendyield2’, start, end)

for code in dfStockDiv.columns:

for idx in dfStockDiv.index:

ifpd.isnull(dfStockDiv.loc[idx, code]) or dt.datetime.strptime(idx,"%Y%m%d") <= dfIPO_Date.loc[code, 'OneYearAfter']:

                idIndex =dfStockVsIndex.loc[code, 'AMAC']

                dfStockDiv.loc[idx, code] =dfIndexDiv.loc[idx, idIndex]

   dfStockDiv.index = [rs.yyyymmdd2str(t) for t indfStockDiv.index]

return dfStockDiv / 100.0

最后,是AAP估值数据。可以看到,这个函数才是最简单的,没有太多值得解释的地方。但也请注意,这里是为表达清楚,因此用了三个参数都是数值的模式——问题随之而来,这样就无可避免在最后的批量运算中调用两层嵌套的循环,显然会有效率问题,因此我们在最后使用时,实际在这里进行了调整,需要用类似矩阵运算的方法处理。

defaapDisc(q, v, t):

   '''q:股息率(单位不是%)

   v:波动率(单位不是%)

   t:剩余时间(单位是年)'''

   v2t = v**2*t

   d = np.sqrt(v2t + np.log(2 * (np.exp(v2t) - v2t - 1)) - 2 *np.log(np.exp(v2t) - 1))

   discRatio = np.exp(-q*t) * (stats.norm.cdf(d/2.0) -stats.norm.cdf(-d/2.0))

return discRatio

完成整个计算过程后的一点感受:

1、AAP估值和以前的摊余成本法真正的区别是什么?简单来说,老算法基本依赖于成本价和锁定时间,新的算法下,估值与成本价已经基本无关,取而代之的是锁定时间、波动率和股息率。这也意味着:1)无论配售和定增,基金净值都有可能在拿到股份的节点,出现跳升(当然定价不合理也)。因此配售\定增发生的预期,对基金价值的影响会比较大;2)以及,定增的定价上,如果要引入公募基金,那么这个定价的折扣就不能低于AAP估值法太多,否则参与即浮亏,在基金这一端是比较难以接受的。

2、现行算法下,什么是重要的?三要素变成了波动率、锁定期和股息率。我们观察,最不重要的是股息率,最重要的是锁定期,波动率居于二者之间。

固收+ 市场跟踪

1、分级A市场方面,分级A指数近10个交易日上涨0.2%,其中此前大涨的R+3.5%指数回落,跌0.13%,而R+3.0%指数涨幅达到0.26%。主流品种整体差异不大,军工A上涨0.49%,而深成指A仍有0.24%的涨幅。

对于分级A市场,我们保持近期观点,目前市场平均折价仍较低,不具备鲜明的投资特性,因而对于博弈性投资者而言,分级A价值不大。但毕竟绝对收益率水平不低且股市的波幅实际已经变窄,加大了分级A产品的债券成分,因此存在债券替代价值。品种上,可关注券商A及深成指A。

2、打新基金:近期,由于科创板新股涨幅整体降低,打新基金的无风险收益已经有所降低。但11月新股上市的数量较多,也有部分个股的上市表现较好,因此我们筛选的打新基金样本池11月普遍表现出了正增长。

3、配售基金近10日平均下跌0.88%,与市场预期走势比较吻合。此前市场开始预期银行股的战略配售,配售基金随之上涨。不过近期市场情绪不佳,新股上市出现破发、涨幅偏低等情况,配售基金近期也出现回落。

定增基金分化稍大,平均涨幅1%,其中九泰瑞富、九泰泰富涨幅超过2%。整体上,定增基金的名义、隐含折价率有小幅压缩,但并不明显。

4、转债市场方面,相比股市来讲,转债指数的振幅要小得多,近10日小幅下跌0.32%。背后原因在于,近期调整的主力军消费白马,在转债中分布不多,即便存在,其所占权重也很低。而真正主导转债指数的,仍是银行转债。总体上,我们在8月底认为转债买入窗口关闭,机会减少,近期仍保持这一观点。但近期股市情绪大幅回落,转债新券上市的价位、估值也开始和老券拉开距离,我们建议投资者开始关注转债市场估值调整以及低估值品种数量,尤其12月中旬有可能随着低价新券上市增多,转债市场机会可能重新出现。

5、ABS市场方面,临近年底银行间市场的发行节奏有所加快。在多个资产共同发力的情况下,10-11月的发行规模已经突破2800亿。如果12月的发行节奏依然较快,则全年发行量仍保留了超预期的可能性,12月可能迎来一个投资价格较好的时点。

价格方面,在超预期的供给暂时还未出现之前,价格仍然保持在低位。

6、REITs市场方面,国内共有6支基金主要投资于REITs领域。其中,上投摩根、南方、广发的三只基金别分跟踪了3个较为主流的REITs指数;诺安、嘉实、鹏华的三只基金则为主动投资。

此前美国REITs市场出现较大的调整,近两周价格表现有了一定的恢复,各基金净值均出现了正增长。

本文所引为报告部分内容,报告原文请见2019年11月29日中金固定收益研究发表的研究报告。

免责声明:自媒体综合提供的内容均源自自媒体,版权归原作者所有,转载请联系原作者并获许可。文章观点仅代表作者本人,不代表新浪立场。若内容涉及投资建议,仅供参考勿作为投资依据。投资有风险,入市需谨慎。

限售股 转债

热门推荐

收起
新浪财经公众号
新浪财经公众号

24小时滚动播报最新的财经资讯和视频,更多粉丝福利扫描二维码关注(sinafinance)

7X24小时

  • 12-11 甬金股份 603995 22.52
  • 12-04 芯源微 688037 --
  • 12-04 锐明技术 002970 --
  • 12-03 成都燃气 603053 --
  • 12-02 当虹科技 688039 50.48
  • 股市直播

    • 图文直播间
    • 视频直播间