QuantLib中如何实现重置频率与支付频率不同的Index?
结论
如果 CNYRepo7D 表示期限为 7 天、每 7 天重新定盘的利率,就不应仅通过改名把它创建为 OvernightIndex。OvernightIndex 表示的仍是隔夜指数,名称中的 7D 不会把复利周期改成 7 天。
对于“每 7 天重置、每季度支付”的浮动端,应采用:
IborIndex,将 tenor 设置为 7 天,表示每次定盘对应的利率期限;SubPeriodsCoupon/SubPeriodsLeg,在每个季度支付期内生成多个 7 天子期间;RateAveraging.Compound,用于合同约定对子期间利率进行复利的情况;如果合同采用算术平均,则使用RateAveraging.Simple。
普通 IborCoupon,或由 NonstandardSwap 根据季度浮动端 schedule 创建的 coupon,通常只会在每个季度 coupon 期取得一次 fixing。即使 index tenor 是 7 天,也不会自动在季度内生成一系列 7 天重置。
为什么三个结果看起来相同
通常有两个原因。
1. Index tenor 不等于 coupon reset schedule
IborIndex(..., ql.Period(7, ql.Days), ...) 中的 tenor 描述一次 index fixing 对应的价值期长度,并不表示:
在一个三个月 coupon 内,自动每 7 天重新定盘一次。
一个支付期内有多少次 fixing,取决于 coupon 类型及其内部的子期间安排。普通 IborCoupon 是“一张 coupon、一次 fixing”,SubPeriodsCoupon 才是“一张 coupon、多个子期间 fixing”。
所以,只把 index 从 7D 改成 3M,并不能建立“季度内每周重置”的结构。
2. Flat curve 会掩盖频率差异
在同一条无套利预测曲线上,如果每个子期间的远期利率都由折现因子推导,则有:
[ 1 + F_i\tau_i=\frac{P(t_i)}{P(t_{i+1})} ]
对子期间进行复利:
[ \prod_i(1+F_i\tau_i)
\prod_i\frac{P(t_i)}{P(t_{i+1})}
\frac{P(T_0)}{P(T_n)} ]
中间的折现因子会完全抵消。因此,在没有 spread、cap/floor、历史 fixing 差异、观察期调整或多曲线差异的情况下,以下几种计算方式可能得到相同或几乎相同的 coupon 金额:
- 每日复利;
- 每周复利;
- 整个季度使用一个由同一曲线推导的远期利率。
这不代表三种产品结构相同,只能说明这个 flat-curve 测试不足以区分它们。
正确的建模结构
每季度只重置一次
如果合同规定每季度支付,并且每季度只取得一次 fixing,可以使用普通 IborIndex 和 IborLeg。此时 index tenor 通常与 coupon tenor 一致,例如 3M:
index_3m = ql.IborIndex(
"CNY-3M",
ql.Period(3, ql.Months),
1,
ql.CNYCurrency(),
ql.China(),
ql.ModifiedFollowing,
False,
ql.Actual365Fixed(),
curve_handle
)
每 7 天重置、每季度复利支付
先定义 7 天 index:
index_7d = ql.IborIndex(
"CNYRepo7D",
ql.Period(7, ql.Days),
1,
ql.CNYCurrency(),
ql.China(),
ql.ModifiedFollowing,
False,
ql.Actual365Fixed(),
curve_handle
)
然后以季度 schedule 划分支付边界,通过 SubPeriodsLeg 在每个季度 coupon 内建立 7 天子期间:
payment_schedule = ql.Schedule(
today,
maturity_date,
ql.Period(3, ql.Months),
calendar,
ql.ModifiedFollowing,
ql.ModifiedFollowing,
ql.DateGeneration.Forward,
False
)
floating_leg = ql.SubPeriodsLeg(
[1.0],
payment_schedule,
index_7d,
ql.Actual365Fixed(),
calendar,
ql.ModifiedFollowing,
1,
[],
[1.0],
[],
[],
[],
True,
ql.RateAveraging.Compound
)
不同 QuantLib-Python 版本暴露的 SubPeriodsLeg 构造函数和参数顺序可能不同,实际使用前应检查当前环境中的签名:
help(ql.SubPeriodsLeg)
help(ql.SubPeriodsCoupon)
重点不是某个位置参数,而是以下组合:
7D IborIndex
+ quarterly payment schedule
+ SubPeriodsLeg/SubPeriodsCoupon
+ RateAveraging.Compound
如果当前 Python 绑定提供 builder 风格的方法,也可以分别设置 notionals、gearings、spreads、fixing days 和 averaging type。具体写法应以当前安装版本的签名为准。
OvernightIndex 适用于什么情况
如果合同以每日隔夜利率为基础,并将每日利息复利至季度末支付,应使用:
OvernightIndex + OvernightIndexedCoupon / OvernightLeg
这类 coupon 通常还需要处理:
- lookback;
- lockout;
- observation shift;
- rate cutoff;
- telescopic value dates;
- spread 是否参与复利。
如果标的是一个真正的 7 天定盘利率,使用 OvernightIndex 会错误表达 fixing 期限和日期逻辑。把对象命名为 CNYRepo7D 也不会改变其经济含义。
如何验证实现确实每周重置
不要只比较最终 cash flow。应将 coupon 向下转换,并检查其 fixing/value dates。可访问的方法取决于 QuantLib-Python 版本,例如:
for cf in floating_leg:
coupon = ql.as_sub_periods_coupon(cf)
print(
coupon.accrualStartDate(),
coupon.accrualEndDate(),
coupon.date(),
coupon.amount()
)
如果当前绑定暴露了子期间日期或 fixing 日期,也应逐一输出。需要确认的是,每张季度 coupon 内确实包含多个约 7 天的子期间,而不是只有一个季度 fixing。
测试时也不要只使用 flat curve。可以改用:
- 非平坦预测曲线;
- 不同的历史 fixing;
- 子期间 spread;
- 不同的日历调整;
- 跨节假日的 7 天期间。
这些测试条件更容易显示每日复利、每周复利与季度单次 fixing 之间的差异。
其他注意事项
ql.Period(7, ql.Days)最终表示 7 个日历日,还是经过业务日历调整后的期限,取决于 index 的 calendar、business-day convention 和 end-of-month 设置。- 季度末通常会出现不足 7 天的 stub,需要按照合同确认处理方式。
fixingDays=1表示 fixing date 相对于 value date 提前的天数,不是 reset frequency。dayCounter会直接影响每个子期间的 accrual fraction。- 加入 spread 时,需要确认 spread 是在每个子期间参与复利,还是在复合后的总利率上相加。两种方式的结果不同。
NonstandardSwap可以处理不规则名义本金等结构,但不会自动把普通的季度IborCoupon变成每周重置的复合 coupon。对于这类浮动端,通常可以直接构造SubPeriodsLeg,再用ql.Swap与固定端组合,结构会更清楚。
备注:内容仅供参考。
