债券到期收益率如何计算?

某债券四年后到期,其面值为人民币100元,每年付息一次,票面利率7%.若以105元卖出。则其到期收益率多少? 完全搞不懂了,求解啊
关注者
26
被浏览
83,128

8 个回答

本文分析FinancePy的源代码,阐述了各种到期收益率(Yield to Maturity,简称YTM)算法,并对比了各国收益率算法的差异。

1. 万恶的日历

根据知乎这篇回答:为什么金融衍生品的计算中有时候将一年视作 360 天有时候又用 365 天来计算?, 日历是人类史上最糟糕的发明。确实如此,现在的公历对金融计算来说简直是一个灾难。如果说闰年还受制于地球公转速度的话,那么2月只有28天是什么鬼?(古罗马独裁者屋大维的锅)更不用说中国的各种不可预测的连休假期。面对全球各国乌烟瘴气的金融日历,难怪有公司理直气壮地提供年费5000美元的金融日历服务。

Date

FinancePy的解决方法很传统:自己重写一套针对日历的API。它首先重新写了一个长达1000行的Date Class。这个Date有两点值得一说:

  1. 作者提出了一个Issue,说有两种方法实现Date,一是底层完全依赖标准库datetime,Date只是在外围包一层高级API;二是重写所有底层逻辑,完全不依赖datetime。目前的实现主要是用第二种方法,只有少部分调用datetime。作者想知道哪种实现方法的运行速度更快。他猜测重写底层逻辑可能可以受惠于Numba的加速,但不确定,所以想请相关专家志愿者尝试一下两种方案再做对比。然而这个Issue自2020年提出以来至今都没有志愿者响应。
  2. Date的核心思想是把1900年以来的所有日期都换算成相对1900年的天数。这里有一段注释说Excel有个bug,把1900年视作闰年。Excel当年为了与最红的Lotus兼容(抄袭),继承了这个bug。FinancePy为了与Excel兼容,也继承了这个bug…… 万恶的日历属实是把大家都逼疯了。
Calculate list of dates so that we can do quick lookup to get the
number of dates since 1 Jan 1900 (inclusive) BUT TAKING INTO ACCOUNT THE
FACT THAT EXCEL MISTAKENLY CALLS 1900 A LEAP YEAR. For us, agreement with
Excel is more important than this leap year error and in any case, we will
not usually be calculating day differences with start dates before 28 Feb
1900. Note that Excel inherited this "BUG" from LOTUS 1-2-3.

DayCount

接下来是DayCount Class。DayCount实际上只有一个函数year_frac(),这个函数又花费了接近300行。它的作用是计算两个日期之间相当于多少年。为什么实现如此简单的功能需要这么多代码?因为它实现了9种conventions:

THIRTY_360_BOND = 1  # 30E/360 ISDA 2006 4.16f, German, Eurobond(ISDA 2000)  
THIRTY_E_360 = 2  # ISDA 2006 4.16(g) 30/360 ISMA, ICMA  
THIRTY_E_360_ISDA = 3  # ISDA 2006 4.16(h)  
THIRTY_E_PLUS_360 = 4  # ROLLS D2 TO NEXT MONTH IF D2 = 31  
ACT_ACT_ISDA = 5  # SPLITS ACCRUAL PERIOD INTO LEAP YEAR AND NON LEAP YEAR  
ACT_ACT_ICMA = 6  # METHOD FOR ALL US TREASURY NOTES AND BONDS  
ACT_365F = 7  # Denominator is always Fixed at 365, even in a leap year  
ACT_360 = 8  
ACT_365L = 9  # the 29 Feb is counted if it is in the date range

根据上面的知乎大佬的介绍,Matlab自带了12种Convention,Excel则自带了5种,而这12+5种竟然没有一种是重复的。大佬的一位巨佬同事看不下去,闲着没事总结出目前存在于各市场的36种日期算法,在此基础上再推出18种未被市场使用的日期算法……

这里介绍一下ACT_ACT_ISDA,跨闰年的时候计算方法比较特别。

elif self._type == DayCountTypes.ACT_ACT_ISDA:  
    if is_leap_year(y1):  
        denom1 = 366  
    else:  
        denom1 = 365  
    if is_leap_year(y2):  
        denom2 = 366  
    else:  
        denom2 = 365  
    if y1 == y2:  
        num = dt2 - dt1  
        den = denom1  
        acc_factor = (dt2 - dt1) / denom1  
        return (acc_factor, num, den)  
    else:  
        daysYear1 = datediff(dt1, Date(1, 1, y1 + 1))  
        daysYear2 = datediff(Date(1, 1, y2), dt2)  
        acc_factor1 = daysYear1 / denom1  
        acc_factor2 = daysYear2 / denom2  
        yearDiff = y2 - y1 - 1.0  
        # Note that num/den does not equal acc_factor  
        # I do need to pass num back        
        num = daysYear1 + daysYear2  
        den = denom1 + denom2  
        acc_factor = acc_factor1 + acc_factor2 + yearDiff  
        return (acc_factor, num, den)

举一个跨闰年例子:如果起始日在非闰年,结束日在闰年,则非闰年那段日期用365作分母,闰年那段日期用366作分母,两段日期年化之后相加,则是整段时间的年化年数。

中国的ACT/ACT对应的标准应该是ACT_ACT_ICMA,即实际天数 / 当个计息周期的实际天数。

2. 应计利息

本来解决了日期之后,应计利息应该是手到擒来很容易才对,但FinancePy有两个地方令我花费了大量时间理解:一是它考虑了节假日继续付息等奇怪规则(这点没有深究),二是它在算应计利息的时候,顺便算了一个叫做alpha的东西,这个alpha在之后算收益率的时候作用很大。

FinancePy里,凡是和计算债券收益率有关的函数,都会调用Bond.calc_accrued_interest()。calc_accrued_interest()返回应计利息,同时修改Bond内置的alpha和应计利息天数。个人觉得这种设计不太好,alpha和应计利息一样,都是在特定结算日下的产物,和Bond本身关系不大,应该作为calc_accrued_interest的返回值之一,而不是作为Bond的属性被修改。可能作者在其他计算里为方便暂存alpha,所以才这样设计。

Alpha

看源码的时候,我一直在琢磨这个alpha究竟是什么。

alpha=\frac{结算日至下一个付息日的天数}{当前付息周期的天数}

如果每年只付息一次,那很容易计算,但如果每年付息多次,那就不简单,见下图:

FinancePy 中的 Alpha


year_frac()算的是上图中的 x/T,我们要计算的alpha是d/TS。如果每年的付息频率是f,那么容易得出:alpha=1-\frac{x}{T}*f

3. 到期收益率

计算ytm的函数为Bond.yield_to_maturity(),只接受净价不接受全价。函数通过calc_accrued_interest算出全价和alpha,然后用牛顿法逼近ytm:

argtuple = (self, settlement_date, full_price, convention)  
ytm = optimize.newton(_f,  
                      x0=0.05,  # guess initial value of 5%  
                      fprime=None,  
                      args=argtuple,  
                      tol=1e-8,  
                      maxiter=50,  
                      fprime2=None)

这里的_f其实调用了full_price_from_ytm() ,把price = f(ytm)这个函数变形成 f(ytm)-price = 0,便于牛顿法求解。

各国算法

FinancePy目前提供了3种YTM规则:

  1. UK DMO。这是英国国债的YTM规则,作者应该是英国人。DMO全称Debt Management Office,专门帮英国财政部发行国债。英国财政部全称叫做 HM Treasury 。这个HM以前是Her Majesty’s,现在是His Majesty’s Treasury。难怪官网只称自己为HM Treasury,无懈可击。
  2. US Treasury。顾名思义,这是美国国债的YTM规则。
  3. US Street。这个不知是什么意思,难道是Wall Street规则?这个规则和中国央行规则最接近。

付息周期残差

没错,痛苦的根源又来自于日期。这个付息周期残差即前文的alpha。各种规则都围绕alpha对贴现因子的影响做文章。

周期残差可以这样影响贴现因子:

pv=\frac{cash flow}{1+ytm*\alpha} \\

也可以这样影响:

pv=\frac{cash flow}{(1+ytm)^{\alpha}} \\

第一个简称“直乘”,第二个简称“次方”,则FinancePy中的3种规则如下表所示:

规则只剩最后一个现金流超过一个现金流
UK DMO次方次方
US Treasury次方直乘
US Street直乘次方

中国央行规则很像US Street规则,不同之处在于:中国规则对于剩余期限在1年以上的最后一个付息周期,使用“次方”法。不过实际市场中很少出现剩余期限在1年以上的到期一次还本付息债券或贴现债券,我职业生涯好像从未遇过。

US Street 规则

对于只剩最后一个现金流的情况,计算公式为:

pv=\frac{cash flow}{1+\frac{ytm}{f}*\alpha} \\

对于还剩余n个现金流的情况,计算公式为:

pv=\frac{cash_{1}}{(1+ytm/f)^{\alpha}}+\frac{cash_{2}}{(1+ytm/f)^{\alpha+1}}+...+\frac{cash_{n}}{(1+ytm/f)^{\alpha+n}} \\

f是每年的付息频率

4. 解方程不是一件容易的事情

即使在2022年,在Python各种强大的计算库(比如Scipy)的加持下,解方程仍然不是一件容易的事情。YTM的折现方程够简单了吧,然而我在骑乘矩阵计算中(详见:固定收益交易策略系统(三))发现牛顿法经常失败,主要是初始点不好估计,而且这个折现函数似乎不那么简单。看看下面的若隐若现的函数曲线动画:

我总不能为了求解YTM的方程先去解一个初始点的估算方程吧,于是我一气之下改成了二分法。Scipy里有个改良版二分法叫做Brentq,但是实测还没@张神马钊 写的原始二分法快。

FinancePy用的原始牛顿法解YTM方程,初始尝试值固定为5%,正常情况够用,但遇到稍微极端一点的条件估计就完蛋了。

到期收益率=(收回金额-购买价格+总利息)/(购买价格×到期时间)×100

示例:

某种债券面值100元,10年还本,年息8元,名义收益率为8%,如该债券某日的市价为95元,则当期收益率为8/95,若某人在第一年年初以95元市价买进面值100元的10年期债券,持有到期,则9年间除每年获得利息8元外,还获得本金盈利5元,到期收益率为(8×9+5)/(95×10)。

别处找的,8点多吧