PAYATHON 2026

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 与固定端组合,结构会更清楚。

备注:内容仅供参考。