文章

QuantLib 金融计算——案例之普通利率互换分析(1)

介绍用 QuantLib 对存续的普通利率互换合约估值。

由于版本问题,代码可能与最新版不兼容。

QuantLib 金融计算——案例之普通利率互换分析(1):对存续合约估值

概述

QuantLib 中涉及利率互换的功能大致分为两大类:

  • 对存续的利率互换合约估值;
  • 根据利率互换合约的成交报价推算隐含的期限结构。

这两类功能是紧密联系的,根据最新报价推算出的期限结构通常可以用来对存续合约进行估值。

本文接下来介绍如何具体实现对合约的估值,并以 Real world tidy interest rate swap pricing 中 Bloomberg 的结果作为比较基准。

Bloomberg 的结果:

合约条款

对存续的利率互换合约进行估值,通常是根据当前的期限结构计算出浮动端(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

实践

1
2
3
4
5
6
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 DateDiscount Factors
04/15/2019NA
04/23/20191.0000735
05/16/20191.0003059
07/16/20191.0007842
10/16/20191.0011807
04/16/20201.0023373
10/16/20201.0033115
04/16/20211.0039976
04/19/20221.0039393
04/17/20231.0015958
04/16/20240.9972325
04/16/20250.9907452
04/16/20260.9820912
04/16/20270.9715859
04/18/20280.9591332
04/16/20290.9455427
04/16/20300.9311096
04/16/20310.9161298
04/17/20340.8705738
04/18/20390.8017461
04/19/20440.7464983
04/20/20490.7010373
04/16/20540.6626670
04/16/20590.6289098
04/16/20640.5974307
04/16/20690.5684840
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 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 的利率,否则会报错,幸运的是它们不参与估值,可以用 0 来填充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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)

注:Euribor6MIborIndex 的派生类。

设置合约

一些基本设置:

1
2
3
4
5
6
7
8
# 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),计算出现金流的发生日期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 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,并根据前面配置好的现金流日期计算浮动端和固定端的预期贴现现金流。

1
2
3
4
5
6
7
8
9
10
11
12
13
spot25YearSwap = ql.VanillaSwap(
    swapType,
    nominal,
    fixedSchedule,
    fixedRate,
    fixedLegDayCounter,
    floatSchedule,
    euriborIndex,
    spread,
    floatingLegDayCounter)

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

估值

Bloomberg 对浮动端和固定端的估值考虑了本金,而 QuantLib 默认不考虑本金,所以浮动端和固定端的 NPV 要自己计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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)

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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 如何处理这种不一致也不得而知。

下一步

  • 分析国内市场上的利率互换。
  • 从利率互换的成交报价中推算期限结构。
本文由作者按照 CC BY 4.0 进行授权