# QuantLib 金融计算——案例之浮息债（挂钩 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$$：距离下一付息日的天数占当前付息周期长度的比例

## 浮息债的久期和凸性

### 利差久期和利差凸性

• 利差久期：

$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} \left( \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}$

### 利率久期和利率凸性

• 利率久期：

$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} \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\\ &- \frac{1}{PV}\frac{1}{f} \left( \sum_{i=1}^n \frac{1}{(1+(R+y)/f)^{t+i}} \right)\times 100\\ =&D_y - \frac{1}{PV}\frac{1}{f} \left( \sum_{i=1}^n \frac{1}{(1+(R+y)/f)^{t+i}} \right)\times 100 \end{aligned} \tag{3}

$\Sigma = \frac{1}{f} \left( \sum_{i=1}^n \frac{1}{(1+(R+y)/f)^{t+i}} \right)\times 100$

• 利率凸性

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

$\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}$

$\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} \left( \sum_{i=1}^n \frac{t+i}{(1+(R+y)/f)^{t+i+1}} \right)\times 100$

## 计算案例

### 价格与现金流

• 债券起息日：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)
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


• 首先，要把 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)
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)

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

bondYield = 3.7179 / 100.0

lprTermStructure,
compounding,
frequency,
accrualDayCounter))

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.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 |
+------------+----------+
'''


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

tab = pt.PrettyTable(['item', 'value'])

tab.float_format = '.4'

print(tab)

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


### 久期与凸性

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


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

bond,
bondYield,
accrualDayCounter,
compounding,
frequency,
ql.Duration.Modified)

bond,
bondYield,
accrualDayCounter,
compounding,
frequency)

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

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

'解析结果',

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)

dp1 = bond.dirtyPrice()
dp2 = bond.dirtyPrice()

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

'数值结果',

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

'''
+----------+-------------+-------------+
|   项目   |   解析结果  |   数值结果  |
+----------+-------------+-------------+
| 利差久期 |  4.37254790 |  4.37254808 |
| 利差凸性 | 21.08466334 | 21.08466386 |
| 利率久期 |  0.17188881 |  0.17188881 |
| 利率凸性 | -0.11051093 | -0.11051092 |
+----------+-------------+-------------+
'''


## 参考文献

• 《浮动利率债券收益率计算与风险分析》
• 《浮动利率债券久期和凸性的研究》
• 《浮动利率债券的基准利率选择及定价》
• 《中债价格指标产品久期基本计算方法》
• 《浮动利率债券定价的理论与实践》

## 扩展阅读

《QuantLib 金融计算》系列合集

posted @ 2020-09-21 16:54  xuruilong100  阅读(2216)  评论(2编辑  收藏  举报