天天看點

QuantLib 金融計算——案例之普通利率互換分析(1):對存續合約估值

目錄

  • QuantLib 金融計算——案例之普通利率互換分析(1)
    • 概述
    • 合約條款
    • 實踐
      • 設定期限結構
      • 添加曆史浮動利率
      • 設定合約
      • 估值
    • 估值差異可能的來源
    • 下一步
    • 擴充閱讀

QuantLib 中涉及利率互換的功能大緻分為兩大類:

  • 對存續的利率互換合約估值;
  • 根據利率互換合約的成交報價推算隐含的期限結構。

這兩類功能是緊密聯系的,根據最新報價推算出的期限結構通常可以用來對存續合約進行估值。

本文接下來介紹如何具體實作對合約的估值,并以 Real world tidy interest rate swap pricing 中 Bloomberg 的結果作為比較基準。

Bloomberg 的結果:

QuantLib 金融計算——案例之普通利率互換分析(1):對存續合約估值

對存續的利率互換合約進行估值,通常是根據目前的期限結構計算出浮動端(floating leg)和固定端(fixed leg)的“預期貼現現金流”,兩者之差即合約的估值。需要注意的是,利率互換的估值對合約條款比較敏感。

示例中的合約是一個 Euribor 6M 的利率互換,條款細則如下:

  • 浮動利率:Euribor 6M
  • 固定利率:0.059820%
  • 利差:0.0%
  • 生效期:2007-01-19
  • 期限:25 Y
  • 類型:支付浮動利率,收取固定利率
  • 浮動端支付頻率:半年一次
  • 浮動端天數計算規則:ACT/360
  • 固定端支付頻率:一年一次
  • 固定端天數計算規則:30U/360
  • 月曆:TARGET(比對 Trans-European Automated Real-time Gross Settlement Express Transfer System 的月曆)
  • 估值日期:2019-04-15

import QuantLib as ql
import prettytable as pt

calendar = ql.TARGET()
evaluationDate = ql.Date(15, ql.April, 2019)
ql.Settings.instance().evaluationDate = evaluationDate
           

估值的核心是目前的期限結構,根據 Real world tidy interest rate swap pricing 中的貼現因子資料設定估值用的期限結構。

Maturity Date Discount Factors
04/15/2019 NA
04/23/2019 1.0000735
05/16/2019 1.0003059
07/16/2019 1.0007842
10/16/2019 1.0011807
04/16/2020 1.0023373
10/16/2020 1.0033115
04/16/2021 1.0039976
04/19/2022 1.0039393
04/17/2023 1.0015958
04/16/2024 0.9972325
04/16/2025 0.9907452
04/16/2026 0.9820912
04/16/2027 0.9715859
04/18/2028 0.9591332
04/16/2029 0.9455427
04/16/2030 0.9311096
04/16/2031 0.9161298
04/17/2034 0.8705738
04/18/2039 0.8017461
04/19/2044 0.7464983
04/20/2049 0.7010373
04/16/2054 0.6626670
04/16/2059 0.6289098
04/16/2064 0.5974307
04/16/2069 0.5684840
# discount curve

curveDates = [
    ql.Date(15, ql.April, 2019), ql.Date(23, ql.April, 2019), ql.Date(16, ql.May, 2019), ql.Date(16, ql.July, 2019),
    ql.Date(16, ql.October, 2019), ql.Date(16, ql.April, 2020), ql.Date(16, ql.October, 2020), ql.Date(16, ql.April, 2021),
    ql.Date(19, ql.April, 2022), ql.Date(17, ql.April, 2023), ql.Date(16, ql.April, 2024), ql.Date(16, ql.April, 2025),
    ql.Date(16, ql.April, 2026), ql.Date(16, ql.April, 2027), ql.Date(18, ql.April, 2028), ql.Date(16, ql.April, 2029),
    ql.Date(16, ql.April, 2030), ql.Date(16, ql.April, 2031), ql.Date(17, ql.April, 2034), ql.Date(18, ql.April, 2039),
    ql.Date(19, ql.April, 2044), ql.Date(20, ql.April, 2049), ql.Date(16, ql.April, 2054), ql.Date(16, ql.April, 2059),
    ql.Date(16, ql.April, 2064), ql.Date(16, ql.April, 2069)]

discountFactors = [
    1.0, 1.0000735, 1.0003059, 1.0007842, 1.0011807, 1.0023373, 1.0033115,
    1.0039976, 1.0039393, 1.0015958, 0.9972325, 0.9907452, 0.9820912, 0.9715859,
    0.9591332, 0.9455427, 0.9311096, 0.9161298, 0.8705738, 0.8017461, 0.7464983,
    0.7010373, 0.6626670, 0.6289098, 0.5974307, 0.5684840]

discountCurve = ql.DiscountCurve(
    curveDates,
    discountFactors,
    ql.Actual360(),  # 與浮動端一緻
    calendar)

discountCurveHandle = ql.YieldTermStructureHandle(discountCurve)
           

估值利率互換需要用到一個重要的類——

IborIndex

,它負責根據期限結構以及合約的條款推算出隐含的遠期利率,進而得到浮動端的預期現金流。

由于是對存續合約估值,需要為期限結構添加“曆史浮動利率”——曆史上 fixing date 上的 Euribor 6M 資料。盡管隻有最近一次 fixing 的 Euribor 6M 利率會參與估值,但使用者還是要添加更早期 fixing date 的利率,否則會報錯,幸運的是它們不參與估值,可以用

來填充。

euriborIndex = ql.Euribor6M(discountCurveHandle)

# add fixing dates and rates for floating leg

unusedRate = 0.0  # not used in pricing
rate20190117 = -0.00236  # euribor-6M at 2019-01-17

euriborIndex.addFixing(fixingDate=ql.Date(17, ql.January, 2007), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.July, 2007), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.January, 2008), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.July, 2008), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(15, ql.January, 2009), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(16, ql.July, 2009), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(15, ql.January, 2010), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(15, ql.July, 2010), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.January, 2011), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(15, ql.July, 2011), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.January, 2012), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.July, 2012), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.January, 2013), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.July, 2013), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(16, ql.January, 2014), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.July, 2014), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(15, ql.January, 2015), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(16, ql.July, 2015), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(15, ql.January, 2016), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(15, ql.July, 2016), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.January, 2017), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.July, 2017), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.January, 2018), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.July, 2018), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.January, 2019), fixing=rate20190117)
           
注:

Euribor6M

IborIndex

的派生類。

一些基本設定:

# swap contract

nominal = 10000000.0
spread = 0.0
swapType = ql.VanillaSwap.Receiver
lengthInYears = 25
effectiveDate = ql.Date(19, ql.January, 2007)
terminationDate = effectiveDate + ql.Period(lengthInYears, ql.Years)
           

設定固定端與浮動端的支付時間表(schedule),計算出現金流的發生日期:

# fixed leg

fixedLegFrequency = ql.Period(ql.Annual)
fixedLegConvention = ql.ModifiedFollowing
fixedLegDayCounter = ql.Thirty360(ql.Thirty360.USA)
fixedDateGeneration = ql.DateGeneration.Forward
fixedRate = 0.059820 / 100.0

fixedSchedule = ql.Schedule(
    effectiveDate,
    terminationDate,
    fixedLegFrequency,
    calendar,
    fixedLegConvention,
    fixedLegConvention,
    fixedDateGeneration,
    False)

# floating leg

floatingLegFrequency = ql.Period(ql.Semiannual)
floatingLegConvention = ql.ModifiedFollowing
floatingLegDayCounter = ql.Actual360()
floatingDateGeneration = ql.DateGeneration.Forward

floatSchedule = ql.Schedule(
    effectiveDate,
    terminationDate,
    floatingLegFrequency,
    calendar,
    floatingLegConvention,
    floatingLegConvention,
    floatingDateGeneration,
    False)
           

VanillaSwap

類實作了普通利率互換,

VanillaSwap

類将接受一個定價引擎——

DiscountingSwapEngine

,并根據前面配置好的現金流日期計算浮動端和固定端的預期貼現現金流。

spot25YearSwap = ql.VanillaSwap(
    swapType,
    nominal,
    fixedSchedule,
    fixedRate,
    fixedLegDayCounter,
    floatSchedule,
    euriborIndex,
    spread,
    floatingLegDayCounter)

swapEngine = ql.DiscountingSwapEngine(discountCurveHandle)
spot25YearSwap.setPricingEngine(swapEngine)
           

Bloomberg 對浮動端和固定端的估值考慮了本金,而 QuantLib 預設不考慮本金,是以浮動端和固定端的 NPV 要自己計算。

fixedNpv = 0.0
floatingNpv = 0.0

fixedTable = pt.PrettyTable(['date', 'amount'])

for cf in spot25YearSwap.fixedLeg():
    if cf.date() > evaluationDate:
        fixedTable.add_row([str(cf.date()), cf.amount()])
        fixedNpv = fixedNpv + discountCurveHandle.discount(cf.date()) * cf.amount()

fixedNpv = fixedNpv + discountCurveHandle.discount(
    spot25YearSwap.fixedLeg()[-1].date()) * nominal

floatingTable = pt.PrettyTable(['date', 'amount'])

for cf in spot25YearSwap.floatingLeg():
    if cf.date() > evaluationDate:
        floatingTable.add_row([str(cf.date()), cf.amount()])
        floatingNpv = floatingNpv + discountCurveHandle.discount(cf.date()) * cf.amount()

floatingNpv = floatingNpv + discountCurveHandle.discount(
    spot25YearSwap.floatingLeg()[-1].date()) * nominal

npvTable = pt.PrettyTable(['NPVs', 'amount'])
npvTable.add_row(['total', spot25YearSwap.NPV()])
npvTable.add_row(['fixed leg NPV', fixedNpv])
npvTable.add_row(['floating leg NPV', floatingNpv])

npvTable.align = 'r'
npvTable.float_format = '.2'
print('NPVs:')
print(npvTable)
print()

fixedTable.align = 'r'
fixedTable.float_format = '.4'
print('Fixed Leg Cash Flows (no nominal):')
print(fixedTable)
print()

floatingTable.align = 'r'
floatingTable.float_format = '.4'
print('Floating Leg Cash Flows (no nominal):')
print(floatingTable)
           

結果:

NPVs:
+------------------+------------+
|             NPVs |     amount |
+------------------+------------+
|            total | -877065.26 |
|    fixed leg NPV | 9119162.21 |
| floating leg NPV | 9996227.47 |
+------------------+------------+

Fixed Leg Cash Flows (no nominal):
+--------------------+-----------+
|               date |    amount |
+--------------------+-----------+
| January 20th, 2020 | 5965.3833 |
| January 19th, 2021 | 5965.3833 |
| January 19th, 2022 | 5982.0000 |
| January 19th, 2023 | 5982.0000 |
| January 19th, 2024 | 5982.0000 |
| January 20th, 2025 | 5998.6167 |
| January 19th, 2026 | 5965.3833 |
| January 19th, 2027 | 5982.0000 |
| January 19th, 2028 | 5982.0000 |
| January 19th, 2029 | 5982.0000 |
| January 21st, 2030 | 6015.2333 |
| January 20th, 2031 | 5965.3833 |
| January 19th, 2032 | 5965.3833 |
+--------------------+-----------+

Floating Leg Cash Flows (no nominal):
+--------------------+-------------+
|               date |      amount |
+--------------------+-------------+
|    July 19th, 2019 | -11734.4444 |
| January 20th, 2020 |  -9883.8108 |
|    July 20th, 2020 | -10526.4706 |
| January 19th, 2021 |  -8236.3411 |
|    July 19th, 2021 |  -3118.9504 |
| January 19th, 2022 |    290.3520 |
|    July 19th, 2022 |   6002.4970 |
| January 19th, 2023 |  11853.1381 |
|    July 19th, 2023 |  16803.6213 |
| January 19th, 2024 |  22032.9795 |
|    July 19th, 2024 |  27371.4264 |
| January 20th, 2025 |  33134.5740 |
|    July 21st, 2025 |  38526.4090 |
| January 19th, 2026 |  43841.7013 |
|    July 20th, 2026 |  49022.3996 |
| January 19th, 2027 |  54065.4048 |
|    July 19th, 2027 |  58756.3184 |
| January 19th, 2028 |  64707.0763 |
|    July 19th, 2028 |  67946.7395 |
| January 19th, 2029 |  72599.6699 |
|    July 19th, 2029 |  74090.1906 |
| January 21st, 2030 |  78693.2841 |
|    July 19th, 2030 |  77892.3324 |
| January 20th, 2031 |  82544.3792 |
|    July 21st, 2031 |  83194.2799 |
| January 19th, 2032 |  84980.7972 |
+--------------------+-------------+
           

與 Bloomberg 的結果相比盡管非常接近,但還是存在差異,估值差異的來源可能如下:

  • 在期限結構上插值的技術細節不一緻。

    DiscountCurve

    對貼現因子進行對數線性插值,Bloomberg 的技術細節不得而知。
  • 浮動端和固定端的天數計算規則不一緻,而期限結構的天數計算規則與浮動端保持一緻。天數計算規則的不一緻使得同一“日期”對浮動端和固定端來說意味着不同的“時間”,Bloomberg 如何處理這種不一緻也不得而知。

  • 分析國内市場上的利率互換。
  • 從利率互換的成交報價中推算期限結構。

《QuantLib 金融計算》系列合集

★ 持續學習 ★ 堅持創作 ★

繼續閱讀