天天看點

QuantLib 金融計算——案例之浮息債(挂鈎 LPR)的價格、久期和凸性

目錄

  • QuantLib 金融計算——案例之浮息債(挂鈎 LPR)的價格、久期和凸性
    • 概述
    • 中債登的估值公式
    • 浮息債的久期和凸性
      • 利差久期和利差凸性
      • 利率久期和利率凸性
    • 計算案例
      • 價格與現金流
      • 久期與凸性
    • 下一步
    • 參考文獻
    • 擴充閱讀

作為利率風險系列的第三篇,本文将依據中債登公布的估值公式,介紹挂鈎 LPR 的浮息債的價格、久期和凸性的計算方法,并依托 QuantLib 和 Python 展示相關的程式設計案例。

貸款市場報價利率(Loan Prime Rate,簡稱 LPR),是由具有代表性的報價行,根據本行對最優質客戶的貸款利率,以公開市場操作利率(主要指中期借貸便利利率)加點形成的方式報價,由中國人民銀行授權全國銀行間同業拆借中心計算并公布的基礎性的貸款參考利率,各金融機構應主要參考 LPR 進行貸款定價。

目前,LPR 包括 1 年期和 5 年期以上兩個品種。每月 20 日(遇節假日順延)上午 9 時 30 分由人民銀行授權全國銀行間同業拆借中心釋出。

最近一年來央行一直在大力推行 LPR,不但推出了挂鈎 LPR 的浮息債,而且推出了 LPR 的利率互換和利率期權(未來将專門論述)。

對于挂鈎 LPR 的浮息債,中債登使用如下估值公式:

\[PV = \left(

\frac{(r+s)/f}{(1+(R+y)/f)^t} +

\sum_{i=1}^n \frac{(R+s)/f}{(1+(R+y)/f)^{t+i}} +

\frac{1}{(1+(R+y)/f)^{t+n}}

\right) \times 100

\]

其中:

  • \(PV\):債券全價
  • \(r\):當期債券基礎利率
  • \(R\):估值日基礎利率
  • \(s\):債券招标利差
  • \(f\):每年付息次數
  • \(y\):點差利率
  • \(R + y\):到期利率(YTM)
  • \(n\):剩餘完整付息周期個數
  • \(t\):距離下一付息日的天數占目前付息周期長度的比例

這與國外教科書中的估值公式有很大不同,國外教科書中的公式通常要利用目前的期限結構推算遠期利率,進而得到預期的未來現金流(浮動票息),再對現金流貼現。中債登的公式可以看做是使用了“水準”的期限結構,如果浮息債挂鈎 Shibor3M 或 FR007,也許可以照搬教科書,因為這兩種利率有對應的 IRS 在交易,且流動性較好,理論上可以推算出 Shibor 和 FR 的期限結構(或遠期利率)。

依據中債登的估值公式,浮息債的價格受到兩個可變參數的影響,分别是 \(R\) 和 \(y\)。是以,浮息債分别就 \(R\) 和 \(y\) 衍生出兩套久期和凸性。

浮息債價格關于點差利率(\(y\))的一階敏感性叫做“利差久期”,二階敏感性叫做“利差凸性”。由于 \(y\) 隻出現在貼現因子中,浮息債的利差久期(凸性)和普通固息債的久期(凸性)别無二緻。

  • 利差久期:

\[D_y = -\frac{\mathrm{d} PV}{\mathrm{d}y} \frac{1}{PV}

\[D_y = \frac{1}{PV}\frac{1}{f}\frac{1}{1+(R+y)/f}

\left(

\frac{t(r+s)/f}{(1+(R+y)/f)^t} +

\sum_{i=1}^n \frac{(t+i)(R+s)/f}{(1+(R+y)/f)^{t+i}} +

\frac{t+n}{(1+(R+y)/f)^{t+n}}

\right) \times 100 \tag{1}

  • 利差凸性:

\[C_y = \frac{\mathrm{d}^2 PV}{\mathrm{d}y^2} \frac{1}{PV}

\[C_y = \frac{1}{PV}\frac{1}{f^2}\frac{1}{(1+(R+y)/f)^2}

\frac{t(t+1)(r+s)/f}{(1+(R+y)/f)^t} +

\sum_{i=1}^n \frac{(t+i)(t+i+1)(R+s)/f}{(1+(R+y)/f)^{t+i}} +

\frac{(t+n)(t+n+1)}{(1+(R+y)/f)^{t+n}}

\right) \times 100 \tag{2}

浮息債價格關于估值日基礎利率(\(R\))的一階敏感性叫做“利率久期”,二階敏感性叫做“利率凸性”。由于 \(R\) 同時出現在貼現因子和現金流中,\(R\) 變化的影響會被抵消掉一部分。是以,浮息債的利率久期(凸性)較利差久期(凸性)通常要小很多。

對浮息債而言,利率的市場風險主要展現在點差利率 \(y\) 的變化上。(這一點和信用利差非常相似)

  • 利率久期:

\[D_{R} = -\frac{\mathrm{d} PV}{\mathrm{d}R} \frac{1}{PV}

\[\begin{aligned}

D_{R} =&\frac{1}{PV}\frac{1}{f}\frac{1}{1+(R+y)/f}

\right) \times 100\\

&- \frac{1}{PV}\frac{1}{f}

\sum_{i=1}^n \frac{1}{(1+(R+y)/f)^{t+i}}

\right)\times 100\\

=&D_y - \frac{1}{PV}\frac{1}{f}

\right)\times 100

\end{aligned} \tag{3}

\[\Sigma = \frac{1}{f}

利率久期等于利差久期減去 \(\Sigma / PV\),可以推測利率久期要比利差久期小很多。

  • 利率凸性

\[C_{R} = \frac{\mathrm{d}^2 PV}{\mathrm{d}R^2} \frac{1}{PV}

根據(3)可以知道

\[\frac{\mathrm{d} PV}{\mathrm{d}R} = \frac{\mathrm{d} PV}{\mathrm{d}y} + \Sigma

是以

\[\frac{\mathrm{d}^2 PV}{\mathrm{d}R^2} = \frac{\mathrm{d}^2 PV}{\mathrm{d}y\mathrm{d}R} + \frac{\mathrm{d}\Sigma}{\mathrm{d}R}\\

\frac{\mathrm{d}^2 PV}{\mathrm{d}R\mathrm{d}y} = \frac{\mathrm{d}^2 PV}{\mathrm{d}y^2} + \frac{\mathrm{d}\Sigma}{\mathrm{d}y}

進而

\[\frac{\mathrm{d}^2 PV}{\mathrm{d}R^2} = \frac{\mathrm{d}^2 PV}{\mathrm{d}y^2} + \frac{\mathrm{d}\Sigma}{\mathrm{d}y} + \frac{\mathrm{d}\Sigma}{\mathrm{d}R}

而對于 \(\Sigma\) 來說,

\[\frac{\mathrm{d}\Sigma}{\mathrm{d}y} = \frac{\mathrm{d}\Sigma}{\mathrm{d}R}

是以

\[\frac{\mathrm{d}^2 PV}{\mathrm{d}R^2} = \frac{\mathrm{d}^2 PV}{\mathrm{d}y^2} + 2\frac{\mathrm{d}\Sigma}{\mathrm{d}y}

最終

\[C_{R} = C_y - 2\frac{1}{PV}\frac{1}{f^2}

\sum_{i=1}^n \frac{t+i}{(1+(R+y)/f)^{t+i+1}}

利率凸性等于利差凸性加 \(2\frac{1}{PV}\frac{\mathrm{d}\Sigma}{\mathrm{d}y}\),可以推測利率凸性要比利差凸性小很多。

下面将以 200218 為例,計算 2020-09-15 這一天的價格、久期和凸性。

首先從中國貨币網查詢債券的基本資訊,用以配置

FloatingRateBond

對象。

  • 債券起息日:2020-06-09
  • 到期兌付日:2025-06-09
  • 債券期限:5 年
  • 面值(元):100.00
  • 計息基準:A/A
  • 息票類型:附息式浮動利率
  • 付息頻率:季
  • 票面利率(%):3.1(目前水準)
  • 基準利率(%):3.85(目前水準)
  • 基準利差(%):-0.75
  • 基準利率名:LPR1Y
  • 利率杠杆:1
  • 提前确定利率的天數:1(沒有查到該項目,不過此項不影響估值計算)
  • 結算方式:T+0(與中債估值的規則保持一緻)
import QuantLib as ql
import prettytable as pt
from datetime import date

today = ql.Date(15, ql.September, 2020)
ql.Settings.instance().evaluationDate = today
evalueDate = ql.Settings.instance().evaluationDate

settlementDays = 0
faceAmount = 100.0

effectiveDate = ql.Date(9, ql.June, 2020)
terminationDate = ql.Date(9, ql.June, 2025)
tenor = ql.Period(ql.Quarterly)
calendar = ql.China(ql.China.IB)
convention = ql.Unadjusted
terminationDateConvention = convention
rule = ql.DateGeneration.Backward
endOfMonth = False

schedule = ql.Schedule(
    effectiveDate,
    terminationDate,
    tenor,
    calendar,
    convention,
    terminationDateConvention,
    rule,
    endOfMonth)

nextLpr = 3.85 / 100.0
nextLprQuote = ql.SimpleQuote(nextLpr)
nextLprHandle = ql.QuoteHandle(nextLprQuote)
fixedLpr = 3.85 / 100.0
           

需要注意的是,月曆采用中國的銀行間市場,遇到假期不調整。

目前,QuantLib 中沒有挂鈎 LPR 的浮息債的直接實作,但是鑒于中債登估值的公式比較簡單,可以用 QuantLib 中的一些元件“模拟”出中債登的估值方法。

  • 首先,要把 LPR1Y “想象”成一種類似 Shibor3M 的短期利率。此時的 \(r\) 就是最新的 LPR1Y 利率;
  • 浮動票息由一個水準的期限結構推算出來,對應利率是 \(R\),也就是到期利率和點差利率的差(實際上就等于最新的 LPR1Y 利率);
  • 貼現因子也由一個水準的期限結構推算出來,對應利率是 \(R+y\),也就是到期利率。
compounding = ql.Compounded
frequency = ql.Quarterly
accrualDayCounter = ql.ActualActual(ql.ActualActual.Bond, schedule)
cfDayCounter = ql.ActualActual(ql.ActualActual.Bond)
paymentConvention = ql.Unadjusted
fixingDays = 1
gearings = ql.DoubleVector(1, 1.0)
benchmarkSpread = ql.DoubleVector(1, -0.75 / 100.0)

cfLprTermStructure = ql.YieldTermStructureHandle(
    ql.FlatForward(
        settlementDays,
        calendar,
        nextLprHandle,
        cfDayCounter,
        compounding,
        frequency))

lprTermStructure = ql.YieldTermStructureHandle(
    ql.FlatForward(
        settlementDays,
        calendar,
        nextLprHandle,
        accrualDayCounter,
        compounding,
        frequency))

lpr3m = ql.IborIndex(
    'LPR1Y',
    ql.Period(3, ql.Months),
    settlementDays,
    ql.CNYCurrency(),
    calendar,
    convention,
    endOfMonth,
    cfDayCounter,
    cfLprTermStructure)

lpr3m.addFixing(ql.Date(8, ql.June, 2020), fixedLpr)
lpr3m.addFixing(ql.Date(8, ql.September, 2020), fixedLpr)

bond = ql.FloatingRateBond(
    settlementDays,
    faceAmount,
    schedule,
    lpr3m,
    accrualDayCounter,
    convention,
    fixingDays,
    gearings,
    benchmarkSpread)

bondYield = 3.7179 / 100.0
basisSpread = bondYield - nextLpr
basisSpreadQuote = ql.SimpleQuote(basisSpread)
basisSpreadHandle = ql.QuoteHandle(basisSpreadQuote)

zeroSpreadedTermStructure = ql.YieldTermStructureHandle(
    ql.ZeroSpreadedTermStructure(
        lprTermStructure,
        basisSpreadHandle,
        compounding,
        frequency,
        accrualDayCounter))

engine = ql.DiscountingBondEngine(zeroSpreadedTermStructure)
bond.setPricingEngine(engine)
           

有三點注意事項:

  • 推算票息和貼現因子的期限結構使用了各自的 day counter,原因出在

    IborIndex

    上,它和前面的

    Schedule

    在有關時間的計算上可能産生不一緻(不算嚴重的 bug,算是個 flaw),具體的原因請閱讀以下兩個連結的内容(連結 1、連結 2)
  • 由于是對存續債券估值,需要為期限結構添加“曆史浮動利率”——曆史上 fixing date 上的 LPR1Y 資料。盡管隻有最近一次 fixing 的 LPR1Y 利率會參與估值,但使用者還是要添加更早期 fixing date 的利率,否則會報錯,幸運的是更早期的曆史利率不參與估值,可以随便用個數來填充。(《案例之普通利率互換分析(1)》也出現了這個情況,可以作為參考閱讀)
  • 計算貼現因子用到了

    ZeroSpreadedTermStructure

    ,這裡的利差就是點差利率 \(y\)。

列印出債券的現金流。

cfTab = pt.PrettyTable(['Date', 'Amount'])

for c in bond.cashflows():
    dt = date(c.date().year(), c.date().month(), c.date().dayOfMonth())
    cfTab.add_row([dt, c.amount()])

cfTab.float_format = '.4'

print(cfTab)

'''
+------------+----------+
|    Date    |  Amount  |
+------------+----------+
| 2020-09-09 |  0.7750  |
| 2020-12-09 |  0.7750  |
| 2021-03-09 |  0.7750  |
| 2021-06-09 |  0.7750  |
| 2021-09-09 |  0.7750  |
| 2021-12-09 |  0.7750  |
| 2022-03-09 |  0.7750  |
| 2022-06-09 |  0.7750  |
| 2022-09-09 |  0.7750  |
| 2022-12-09 |  0.7750  |
| 2023-03-09 |  0.7750  |
| 2023-06-09 |  0.7750  |
| 2023-09-09 |  0.7750  |
| 2023-12-09 |  0.7750  |
| 2024-03-09 |  0.7750  |
| 2024-06-09 |  0.7750  |
| 2024-09-09 |  0.7750  |
| 2024-12-09 |  0.7750  |
| 2025-03-09 |  0.7750  |
| 2025-06-09 |  0.7750  |
| 2025-06-09 | 100.0000 |
+------------+----------+
'''
           

如果 day counter 使用不當,現金流可能與 0.7750 存在肉眼難以發覺的細微差距(但對估值依然可以産生可觀的影響),特别是 2023-09-09 這一天,示例詳見連結 1。

測試一下有關價格的計算。

cleanPrice = bond.cleanPrice()
dirtyPrice = bond.dirtyPrice()
accruedAmount = bond.accruedAmount()

tab = pt.PrettyTable(['item', 'value'])
tab.add_row(['clean price', cleanPrice])
tab.add_row(['dirty price', dirtyPrice])
tab.add_row(['accrued amount', accruedAmount])

tab.float_format = '.4'

print(tab)

'''
+----------------+---------+
|      item      |  value  |
+----------------+---------+
|  clean price   | 97.3292 |
|  dirty price   | 97.3803 |
| accrued amount |  0.0511 |
+----------------+---------+
'''
           

之前的公式推導顯示,浮息債的利差久期(凸性)就是通常意義上的久期(凸性),而利率久期(凸性)則可以在利差久期(凸性)的基礎上添加一個附加項得到。是以,可以針對 LPR 浮息債建立一個

BondFunctions

的派生類,把利率久期(凸性)的計算放到派生類裡面,同時還可以複用

BondFunctions

的函數。

class LprBondFunctions(ql.BondFunctions):
    def __init__(self):
        ql.BondFunctions.__init__(self)

    @staticmethod
    def yieldDuration(bond: ql.FloatingRateBond,
                      bondYield: float,
                      dayCounter: ql.DayCounter,
                      compounding,
                      frequency):
        evalueDate = ql.Settings.instance().evaluationDate
        notOccurred = [
            cf for cf in bond.cashflows() if cf.date() > evalueDate]
        dur = ql.BondFunctions.duration(
            bond,
            bondYield,
            dayCounter,
            compounding,
            frequency,
            ql.Duration.Modified)
        p = bond.dirtyPrice()
        y = ql.InterestRate(
            bondYield,
            dayCounter,
            compounding,
            frequency)
        f = y.frequency()
        sigma = 0.0

        # 如果 len(notOccurred) <= 2,這意味着
        # 目前處于最後一個付息周期
        if len(notOccurred) > 2:
            # 跳過第一個和最後一個日期,因為在最後一個日期,
            # 本金與票息是兩個獨立的現金流
            for i in range(1, len(notOccurred) - 1):
                df = y.discountFactor(
                    evalueDate,
                    notOccurred[i].date())
                sigma += df

        dur -= sigma / p / f * 100.0

        return dur

    @staticmethod
    def yieldConvexity(bond: ql.FloatingRateBond,
                       bondYield: float,
                       dayCounter: ql.DayCounter,
                       compounding,
                       frequency):
        evalueDate = ql.Settings.instance().evaluationDate
        notOccurred = [
            cf for cf in bond.cashflows() if cf.date() > evalueDate]
        conv = ql.BondFunctions.convexity(
            bond,
            bondYield,
            dayCounter,
            compounding,
            frequency)
        p = bond.dirtyPrice()
        y = ql.InterestRate(
            bondYield,
            dayCounter,
            compounding,
            frequency)
        f = y.frequency()
        dSigma = 0.0

        # 如果 len(notOccurred) <= 2,這意味着
        # 目前處于最後一個付息周期
        if len(notOccurred) > 2:
            # 跳過第一個和最後一個日期,因為在最後一個日期,
            # 本金與票息是兩個獨立的現金流
            for i in range(1, len(notOccurred) - 1):
                t = f * dayCounter.yearFraction(
                    evalueDate,
                    notOccurred[i].date())
                df = y.discountFactor(
                    evalueDate,
                    notOccurred[i].date())
                dSigma += t * df

        dSigma /= 1 + bondYield / f
        conv -= 2.0 * dSigma / p / f ** 2 * 100.0

        return conv
           

用解析公式和數值法分别計算久期和凸性,互相驗證。這裡依然用到了

Quote

類的奇妙特性。

compTab = pt.PrettyTable()
compTab.add_column(
    '項目',
    ['利差久期', '利差凸性', '利率久期', '利率凸性'])

spreadDuration = ql.BondFunctions.duration(
    bond,
    bondYield,
    accrualDayCounter,
    compounding,
    frequency,
    ql.Duration.Modified)

spreadConvexity = ql.BondFunctions.convexity(
    bond,
    bondYield,
    accrualDayCounter,
    compounding,
    frequency)

yieldDuration = LprBondFunctions.yieldDuration(
    bond,
    bondYield,
    accrualDayCounter,
    compounding,
    frequency)

yieldConvexity = LprBondFunctions.yieldConvexity(
    bond,
    bondYield,
    accrualDayCounter,
    compounding,
    frequency)

compTab.add_column(
    '解析結果',
    [spreadDuration, spreadConvexity, yieldDuration, yieldConvexity])

bp = 0.01 / 100.0

nextLprQuote.setValue(nextLpr + bp)
dp1 = bond.dirtyPrice()
nextLprQuote.setValue(nextLpr - bp)
dp2 = bond.dirtyPrice()
nextLprQuote.setValue(nextLpr)

yieldDuration = -(dp1 - dp2) / (2.0 * dirtyPrice * bp)
yieldConvexity = (dp1 + dp2 - 2.0 * dirtyPrice) / (dirtyPrice * bp ** 2)

basisSpreadQuote.setValue(basisSpread + bp)
dp1 = bond.dirtyPrice()
basisSpreadQuote.setValue(basisSpread - bp)
dp2 = bond.dirtyPrice()
basisSpreadQuote.setValue(basisSpread)

spreadDuration = -(dp1 - dp2) / (2.0 * dirtyPrice * bp)
spreadConvexity = (dp1 + dp2 - 2.0 * dirtyPrice) / (dirtyPrice * bp ** 2)

compTab.add_column(
    '數值結果',
    [spreadDuration, spreadConvexity, yieldDuration, yieldConvexity])

compTab.float_format = '.8'
print(compTab)

'''
+----------+-------------+-------------+
|   項目   |   解析結果  |   數值結果  |
+----------+-------------+-------------+
| 利差久期 |  4.37254790 |  4.37254808 |
| 利差凸性 | 21.08466334 | 21.08466386 |
| 利率久期 |  0.17188881 |  0.17188881 |
| 利率凸性 | -0.11051093 | -0.11051092 |
+----------+-------------+-------------+
'''
           

和中債登的結果比較一下。

項目
估價收益率(%) 3.7179
估價利差凸性 21.0847
估價全價 97.3803
點差收益率(%) -0.1321
估價利差久期 4.3725
估價利率久期 0.1719
估價利率凸性
QuantLib 金融計算——案例之浮息債(挂鈎 LPR)的價格、久期和凸性

上述實踐雖然成功,但實屬非正常的做法,正規的做法是在 C++ 源代碼層面上為

FloatingRateBond

PricingEngine

BondFunctions

分别建立派生類用于挂鈎 LPR 浮息債的計算,下一步将嘗試這種做法。

  • 《浮動利率債券收益率計算與風險分析》
  • 《浮動利率債券久期和凸性的研究》
  • 《浮動利率債券的基準利率選擇及定價》
  • 《中債價格名額産品久期基本計算方法》
  • 《浮動利率債券定價的理論與實踐》

《QuantLib 金融計算》系列合集

★ 持續學習 ★ 堅持創作 ★

繼續閱讀