目錄
- QuantLib 金融計算——案例之固息債的關鍵利率久期(KRD)
- 概述
- 關鍵利率久期的基本概念
- 從擾動的角度計算 KRD
- 計算案例
- Quote 類和引用帶來的便利
- 參考文獻
- 擴充閱讀
2020-11-16 更新:修正 ActualActual
的誤用。
作為利率風險系列的第二篇,本文将以《Interest Rate Risk Modeling》為藍本,介紹關鍵利率久期(KRD)的基本概念,并依托 QuantLib 展示相關的計算案例。
有關 KRD 的進階内容請見《《Interest Rate Risk Modeling》閱讀筆記——第九章》。
上一篇《案例之固息債的價格、久期、凸性和 BPS》中出現的久期和凸性均是基于到期利率(YTM)的風險度量名額。使用 YTM 分析債券隐含了一個重要假設:利率期限結構上各期限的利率同步變化。這個隐含的假設與現實有所出入,盡管期限結構上各期限的利率變化高度相關,但并非 100% 一緻。顯然,傳統的久期無法描述債券價格對期限結構非平行變化的敏感性。
若要更精細地刻畫債券關于利率變化的敏感性,需要分别考慮不同期限上利率變化對債券價格的影響,這要求把期限結構本身作為一個動态變量。
一個期限結構其實可以看做是一個無限維的向量,任意一個期限均是一個次元。考慮一個無限維的向量是一個高深的數學問題,然而基于經驗觀察,期限結構的平滑性相當好,是以隻需要選取幾個特殊期限作為“錨點”,實踐中就可以幾乎完全把握整個曲線的變化。
關鍵利率久期(KRD)就是債券價格關于這些錨點期限上利率的敏感性,一組 KRD 也就描述了債券價格對期限結構非平行變化的敏感性。
假設根據目前期限結構算出來的債券價格是 \(P\),此時某個關鍵期限 \(K\) 上的利率出現了一個微小的擾動 \(\Delta y\),擾動出現後重新計算出的債券價格是 \(P^{\prime}\),那麼債券價格關于 \(K\) 期限利率的敏感性就近似是
\[\frac{P^{\prime} - P}{P \times \Delta y}
\]
也可以采用精度更高的近似方法,正負擾動對應的價格分别是 \(P^{+}\) 和 \(P^{-}\),敏感性近似是
\[\frac{P^{+} - P^{-}}{2 P \times \Delta y}
為保證期限結構的平滑性,擾動不能隻影響一個特定期限,其影響要平滑地擴散到臨近的期限。在 KRD 分析中,要求擾動以線性遞減的形式擴充到左右相鄰的期限,而不會影響相距更遠的期限。例如,標明 5、7、10 年三個相鄰期限,7 年期上 1 bp 的擾動隻能影響到 5 和 10 年期。并且,7-5 年之間,擾動以每年 0.5 bp 的速度遞減,7-10 年之間,擾動以每年 1/3 bp 的速度遞減。
繼續以上一篇《案例之固息債的價格、久期、凸性和 BPS》中出現的 200205 為例,計算 2020-07-28 這一天的久期和 KRD。
首先從中國貨币網查詢債券的基本資訊,用以配置
FixedRateBond
對象。
- 債券起息日:2020-03-10
- 到期兌付日:2030-03-10
- 債券期限:10 年
- 面值(元):100.00
- 計息基準:A/A
- 息票類型:附息式固定利率
- 付息頻率:年
- 票面利率(%):3.0700
- 結算方式:T+1
import QuantLib as ql
import prettytable as pt
import seaborn as sns
today = ql.Date(28, ql.July, 2020)
ql.Settings.instance().evaluationDate = today
settlementDays = 1
faceAmount = 100.0
effectiveDate = ql.Date(10, ql.March, 2020)
terminationDate = ql.Date(10, ql.March, 2030)
tenor = ql.Period(1, ql.Years)
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)
scheduleEx = ql.Schedule(
effectiveDate,
ql.Date(10, ql.March, 2031),
tenor,
calendar,
convention,
terminationDateConvention,
rule,
endOfMonth)
coupons = ql.DoubleVector(1)
coupons[0] = 3.07 / 100.0
accrualDayCounter = ql.ActualActual(
ql.ActualActual.Bond, scheduleEx)
paymentConvention = ql.Unadjusted
bond = ql.FixedRateBond(
settlementDays,
faceAmount,
schedule,
coupons,
accrualDayCounter,
paymentConvention)
在上海清算所查詢估值。由于使用的是估值,也就是到期利率,是以目前的期限結構用
FlatForward
類表示。對于水準的期限結構而言,遠期利率、即期利率和到期利率三者相等。
bondYield = 3.4124 / 100.0
compounding = ql.Compounded
frequency = ql.Annual
flatCurve = ql.YieldTermStructureHandle(
ql.FlatForward(
settlementDays,
calendar,
bondYield,
accrualDayCounter,
compounding,
frequency))
計算 KRD 的時候需要向目前的期限結構添加關鍵期限上的擾動,為此可以借助 QuantLib 中的
InterpolatedPiecewiseZeroSpreadedTermStructure
類模闆,它需要一個模闆參數
Interpolator
,表示所使用的插值方法類。對于 KRD 的計算來說,選擇
Linear
作為模闆參數,以表示線性插值。
要配置執行個體化後的類
InterpolatedPiecewiseZeroSpreadedTermStructure<Linear>
,需要提供三個核心參數:
- 一個
對象,也就是目前的期限結構,關鍵期限上的擾動将被施加在此期限結構上;Handle<YieldTermStructure>
- 一列
對象,表示關鍵期限上的利率擾動;Handle<Quote>
-
對象,表示擾動對應的關鍵期限。Date
具體到 python 環境下,執行個體化後的類
InterpolatedPiecewiseZeroSpreadedTermStructure<Linear>
被包裝并重命名為
SpreadedLinearZeroInterpolatedTermStructure
類。
在計算 KRD 之前,所有擾動的初始值被設定成零。關鍵期限有 11 個,分别是 6 個月和 1~10 年,均勻地覆寫每個付息周期。
initValue = 0.0
rate6m = ql.SimpleQuote(initValue)
rate1y = ql.SimpleQuote(initValue)
rate2y = ql.SimpleQuote(initValue)
rate3y = ql.SimpleQuote(initValue)
rate4y = ql.SimpleQuote(initValue)
rate5y = ql.SimpleQuote(initValue)
rate6y = ql.SimpleQuote(initValue)
rate7y = ql.SimpleQuote(initValue)
rate8y = ql.SimpleQuote(initValue)
rate9y = ql.SimpleQuote(initValue)
rate10y = ql.SimpleQuote(initValue)
rate6mHandle = ql.QuoteHandle(rate6m)
rate1yHandle = ql.QuoteHandle(rate1y)
rate2yHandle = ql.QuoteHandle(rate2y)
rate3yHandle = ql.QuoteHandle(rate3y)
rate4yHandle = ql.QuoteHandle(rate4y)
rate5yHandle = ql.QuoteHandle(rate5y)
rate6yHandle = ql.QuoteHandle(rate6y)
rate7yHandle = ql.QuoteHandle(rate7y)
rate8yHandle = ql.QuoteHandle(rate8y)
rate9yHandle = ql.QuoteHandle(rate9y)
rate10yHandle = ql.QuoteHandle(rate10y)
spreads = ql.QuoteHandleVector()
spreads.append(rate6mHandle)
spreads.append(rate1yHandle)
spreads.append(rate2yHandle)
spreads.append(rate3yHandle)
spreads.append(rate4yHandle)
spreads.append(rate5yHandle)
spreads.append(rate6yHandle)
spreads.append(rate7yHandle)
spreads.append(rate8yHandle)
spreads.append(rate9yHandle)
spreads.append(rate10yHandle)
dates = ql.DateVector()
dates.append(flatCurve.referenceDate() + ql.Period(6, ql.Months))
dates.append(flatCurve.referenceDate() + ql.Period(1, ql.Years))
dates.append(flatCurve.referenceDate() + ql.Period(2, ql.Years))
dates.append(flatCurve.referenceDate() + ql.Period(3, ql.Years))
dates.append(flatCurve.referenceDate() + ql.Period(4, ql.Years))
dates.append(flatCurve.referenceDate() + ql.Period(5, ql.Years))
dates.append(flatCurve.referenceDate() + ql.Period(6, ql.Years))
dates.append(flatCurve.referenceDate() + ql.Period(7, ql.Years))
dates.append(flatCurve.referenceDate() + ql.Period(8, ql.Years))
dates.append(flatCurve.referenceDate() + ql.Period(9, ql.Years))
dates.append(flatCurve.referenceDate() + ql.Period(10, ql.Years))
termStructure = ql.YieldTermStructureHandle(
ql.SpreadedLinearZeroInterpolatedTermStructure(
flatCurve,
spreads,
dates,
compounding,
frequency,
accrualDayCounter))
債券定價引擎采用最常見的
DiscountingBondEngine
。
engine = ql.DiscountingBondEngine(termStructure)
bond.setPricingEngine(engine)
Quote
類和引用帶來的便利
Quote
在底層 C++ 代碼中,QuantLib 類的構造函數和成員函數大量使用了常引用參數和觀察者模式,這使得作為參數的對象具有了“穿透性”,參數對象值的改變可以靠引用和觀察者模式串聯起來的鍊條影響關聯的所有其他對象。
具體到 KRD 的計算,無需重新配置定價引擎,隻要改變關鍵利率的值就可以自動觸發債券的計算。
擾動的大小定為 1 bp,調用成員方法
setValue
便可改變
SimpleQuote
對象的值。
duration = ql.BondFunctions.duration(
bond,
bondYield,
accrualDayCounter,
compounding,
frequency,
ql.Duration.Modified)
tab = pt.PrettyTable(['item', 'value'])
tab.add_row(['duration', duration])
# calculate KRDs
bp = 0.01 / 100.0
krdSum = 0.0
krds = []
times = []
# 6m KRD
rate6m.setValue(bp)
dirtyPrice1 = bond.dirtyPrice()
rate6m.setValue(-bp)
dirtyPrice2 = bond.dirtyPrice()
rate6m.setValue(initValue)
krd6m = -(dirtyPrice1 - dirtyPrice2) / (2.0 * bp * dirtyPrice)
krdSum += krd6m
krds.append(krd6m)
times.append(0.5)
tab.add_row(['krd6m', krd6m])
# 1y KRD
rate1y.setValue(bp)
dirtyPrice1 = bond.dirtyPrice()
rate1y.setValue(-bp)
dirtyPrice2 = bond.dirtyPrice()
rate1y.setValue(initValue)
krd1y = -(dirtyPrice1 - dirtyPrice2) / (2.0 * bp * dirtyPrice)
krdSum += krd1y
krds.append(krd1y)
times.append(1.0)
tab.add_row(['krd1y', krd1y])
# 2y KRD
rate2y.setValue(bp)
dirtyPrice1 = bond.dirtyPrice()
rate2y.setValue(-bp)
dirtyPrice2 = bond.dirtyPrice()
rate2y.setValue(initValue)
krd2y = -(dirtyPrice1 - dirtyPrice2) / (2.0 * bp * dirtyPrice)
krdSum += krd2y
krds.append(krd2y)
times.append(2.0)
tab.add_row(['krd2y', krd2y])
# 3y KRD
rate3y.setValue(bp)
dirtyPrice1 = bond.dirtyPrice()
rate3y.setValue(-bp)
dirtyPrice2 = bond.dirtyPrice()
rate3y.setValue(initValue)
krd3y = -(dirtyPrice1 - dirtyPrice2) / (2.0 * bp * dirtyPrice)
krdSum += krd3y
krds.append(krd3y)
times.append(3.0)
tab.add_row(['krd3y', krd3y])
# 4y KRD
rate4y.setValue(bp)
dirtyPrice1 = bond.dirtyPrice()
rate4y.setValue(-bp)
dirtyPrice2 = bond.dirtyPrice()
rate4y.setValue(initValue)
krd4y = -(dirtyPrice1 - dirtyPrice2) / (2.0 * bp * dirtyPrice)
krdSum += krd4y
krds.append(krd4y)
times.append(4.0)
tab.add_row(['krd4y', krd4y])
# 5y KRD
rate5y.setValue(bp)
dirtyPrice1 = bond.dirtyPrice()
rate5y.setValue(-bp)
dirtyPrice2 = bond.dirtyPrice()
rate5y.setValue(initValue)
krd5y = -(dirtyPrice1 - dirtyPrice2) / (2.0 * bp * dirtyPrice)
krdSum += krd5y
krds.append(krd5y)
times.append(5.0)
tab.add_row(['krd5y', krd5y])
# 6y KRD
rate6y.setValue(bp)
dirtyPrice1 = bond.dirtyPrice()
rate6y.setValue(-bp)
dirtyPrice2 = bond.dirtyPrice()
rate6y.setValue(initValue)
krd6y = -(dirtyPrice1 - dirtyPrice2) / (2.0 * bp * dirtyPrice)
krdSum += krd6y
krds.append(krd6y)
times.append(6.0)
tab.add_row(['krd6y', krd6y])
# 7y KRD
rate7y.setValue(bp)
dirtyPrice1 = bond.dirtyPrice()
rate7y.setValue(-bp)
dirtyPrice2 = bond.dirtyPrice()
rate7y.setValue(initValue)
krd7y = -(dirtyPrice1 - dirtyPrice2) / (2.0 * bp * dirtyPrice)
krdSum += krd7y
krds.append(krd7y)
times.append(7.0)
tab.add_row(['krd7y', krd7y])
# 8y KRD
rate8y.setValue(bp)
dirtyPrice1 = bond.dirtyPrice()
rate8y.setValue(-bp)
dirtyPrice2 = bond.dirtyPrice()
rate8y.setValue(initValue)
krd8y = -(dirtyPrice1 - dirtyPrice2) / (2.0 * bp * dirtyPrice)
krdSum += krd8y
krds.append(krd8y)
times.append(8.0)
tab.add_row(['krd8y', krd8y])
# 9y KRD
rate9y.setValue(bp)
dirtyPrice1 = bond.dirtyPrice()
rate9y.setValue(-bp)
dirtyPrice2 = bond.dirtyPrice()
rate9y.setValue(initValue)
krd9y = -(dirtyPrice1 - dirtyPrice2) / (2.0 * bp * dirtyPrice)
krdSum += krd9y
krds.append(krd9y)
times.append(9.0)
tab.add_row(['krd9y', krd9y])
# 10y KRD
rate10y.setValue(bp)
dirtyPrice1 = bond.dirtyPrice()
rate10y.setValue(-bp)
dirtyPrice2 = bond.dirtyPrice()
rate10y.setValue(initValue)
krd10y = -(dirtyPrice1 - dirtyPrice2) / (2.0 * bp * dirtyPrice)
krdSum += krd10y
krds.append(krd10y)
times.append(10.0)
tab.add_row(['krd10y', krd10y])
tab.add_row(['krdSum', krdSum])
tab.float_format = '.8'
print(tab)
+----------+------------+
| item | value |
+----------+------------+
| duration | 8.07712202 |
| krd6m | 0.01412836 |
| krd1y | 0.02182248 |
| krd2y | 0.05615594 |
| krd3y | 0.08163788 |
| krd4y | 0.10535800 |
| krd5y | 0.12735475 |
| krd6y | 0.14771823 |
| krd7y | 0.16683071 |
| krd8y | 0.18443629 |
| krd9y | 2.84373153 |
| krd10y | 4.32794826 |
| krdSum | 8.07712244 |
+----------+------------+
債券最大一筆現金流的剩餘期限處在 9~10 年之間,是以 9 和 10 年期利率對債券的影響最大。
理論上,各個 KRD 之和約等于修正久期,這是因為各個關鍵期限上同時發生擾動的話就相當于曲線發生了平行移動。數值結果正好驗證了這一點。
KRD 的曲線圖是這樣的:
sns.lineplot(
x=times, y=krds, markers='o')
- 《Interest Rate Risk Modeling》
- 楊筱燕,《關鍵利率久期計算及執行個體分析》
《QuantLib 金融計算》系列合集
★ 持續學習 ★ 堅持創作 ★