python 股票估值_【中金固收·固收+】隐藏价值的角落:限售股AAP估值及Python实现方法(上)...
发布日期:2021-06-24 13:16:42 浏览次数:3 分类:技术文章

本文共 8090 字,大约阅读时间需要 26 分钟。

来源:中金固定收益研究

作者

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

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

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

或许市场不够重视,但这可能也是隐藏价值的一个角落。配售和定增基金往往会持有一些流通受限的股票,而这些股票估值的方法在2017年经历了一次比较大的变化——根据《证券投资基金投资流通受限股票估值指引(试行)》,限售股适用AAP估值法(亚式看跌期权),取代此前近似摊余成本的方法。目前,主要是定增基金和配售基金涉及比较多这类问题。实事求是地说,即使在以前的简易算法下,虽然公式简单,但数据处理存在一定难度,投资者对于当时(比如2016年)定增基金的净值也存在一定疑虑。而现在公式更为复杂了,对投资者来说黑箱化的程度也自然更高了。在以前的模式下,限售股(比如定增得来的股份)的估值为:V= min(当前价,成本价+ (当前价 - 成本价)* 已持有的时间 / 总锁定期),或者表示如下图。

37311da8811f5cb909fe1b54a726b416.png

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

2bf95d295db6f6ffc1f8e608cb306b3b.png可以看出,想比之前近似摊余成本法的计算模式,这里复杂程度还是要高出不少的。其中,σ为该股票波动率,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。

57933b5f2be32641655bbce3f04baf84.png

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。

22f5ada622de22c16a417e427d437c06.png

719602303fe53f9de4b72d1fd8b40f2f.png

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

6fc5c66506de6200cfa6c2da090899a9.png

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

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

d166ba74bcbdf8fd8fd3dd9b1beff931.png

974ca423d812d568db11b158319a56d7.png

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

74b90e45abbcad08c828f66c8090fcd7.png

725445e1746724fcab8765999e450c6e.png

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

720903491dcfee1d26ab0309e81cce6a.png

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

71d631766af952576c0631a0e1f07ef7.png

a8057cfc4442c28c2489cf4e1886f36d.png

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

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

50cb9a1ebd89718b6066c5236b3467ad.png

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

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

转载地址:https://blog.csdn.net/weixin_33073525/article/details/113966567 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:java文档生成_Java文档自动生成
下一篇:python os.walk如何不遍历隐藏文件_python 获取文件下所有文件或目录os.walk()的实例...

发表评论

最新留言

第一次来,支持一个
[***.219.124.196]2024年04月12日 03时51分28秒