You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Quantlib Python收益率曲线Bootstrap报错:根未被包围问题求助

QuantLib 掉期定价曲线构建 RuntimeError 问题解决

问题现象

运行QuantLib计算掉期价格时,构建收益率曲线阶段抛出以下错误:

RuntimeError: 2nd leg: 1st iteration: failed at 1st alive instrument, pillar April 4th, 2023, maturity April 4th, 2023, reference date September 30th, 2022: root not bracketed: f[0.600742,1.66461] -> [1.241044e-02,1.241044e-02]

已排查情况:

  • 确认利率已除以100,排除该常见问题
  • 检查日期设置无明显错误
  • 仅使用年度期限利率时代码可正常运行

用户原始代码:

from QuantLib import *
import pandas as pd

today = Date(30, September, 2022)
Settings.instance().evaluationDate = today

def create_ql_curve():
    estr_short = pd.DataFrame({'Tenor': [6], 'Rate': [1.7466]})
    estr_long = pd.DataFrame({'Tenor': [1,2,5,10,15,20], 'Rate': [2.2828, 2.547, 2.661, 2.804, 2.85, 2.35]})
    euribor_short = pd.DataFrame({'Tenor': [6], 'Rate': [1.7466]})
    euribor_long = pd.DataFrame({'Tenor': [1,2,5,10,15,20], 'Rate': [2.2828, 2.547, 2.661, 2.804, 2.85, 2.35]})

    ois_helpers = [OISRateHelper(0,
                                 Period(int(tenor), Months),
                                 QuoteHandle(SimpleQuote(rate / 100)),
                                 Eonia(),
                                 YieldTermStructureHandle(),
                                 True)
                   for rate, tenor in zip(estr_short['Rate'], estr_short['Tenor'])]

    ois_helpers += [OISRateHelper(0,
                                  Period(int(tenor), Years),
                                  QuoteHandle(SimpleQuote(rate / 100)),
                                  Eonia(),
                                  YieldTermStructureHandle(),
                                  True)  # telescopicValueDates. When set to True bootstrapping is a lot faster.
                    for rate, tenor in zip(estr_long['Rate'], estr_long['Tenor'])]

    discount_curve = PiecewiseLogCubicDiscount(0, TARGET(), ois_helpers, Actual365Fixed())

    discount_curve.enableExtrapolation()

    helpers = [SwapRateHelper(QuoteHandle(SimpleQuote(rate / 100)),
                              Period(int(tenor), Months),
                              TARGET(),
                              Annual,
                              Unadjusted,
                              Thirty360(),
                              Euribor6M(),
                              QuoteHandle(),
                              Period(0, Days),
                              YieldTermStructureHandle(discount_curve))
               for rate, tenor in zip(euribor_short['Rate'], euribor_short['Tenor'])]

    helpers += [SwapRateHelper(QuoteHandle(SimpleQuote(rate / 100)),
                               Period(int(tenor), Years),
                               TARGET(),
                               Annual,
                               Unadjusted,
                               Thirty360(),
                               Euribor6M(),
                               QuoteHandle(),
                               Period(0, Days),
                               YieldTermStructureHandle(discount_curve))
                for rate, tenor in zip(euribor_long['Rate'], euribor_long['Tenor'])]

    euribor_curve = PiecewiseLogCubicDiscount(0, TARGET(), helpers, Actual365Fixed())

    euribor_curve.enableExtrapolation()

    return discount_curve, euribor_curve

discount_curve, euribor_curve = create_ql_curve()

forecast_handle = RelinkableYieldTermStructureHandle(euribor_curve)
discount_handle = RelinkableYieldTermStructureHandle(discount_curve)

index_6M = Euribor6M(forecast_handle)
swap_engine = DiscountingSwapEngine(discount_handle)

#  swaps

start_date = Date(23, April, 2014)
maturity_date = Date(23, April, 2023)
fixed_schedule = Schedule(start_date, maturity_date,
                          Period(1, Years), TARGET(), Unadjusted, Unadjusted,
                          DateGeneration.Forward, False)

floating_schedule = Schedule(start_date, maturity_date,
                             Period(6, Months), TARGET(), ModifiedFollowing, ModifiedFollowing,
                             DateGeneration.Forward, True)

notional = 115000000
fixed_rate = 0.01727
fixed_leg_daycount = Actual360()
float_spread = 0
float_leg_daycount = Actual360()

calendar = TARGET()
previous = calendar.advance(today, -27, Weeks)
dates = [calendar.advance(previous, n, Days) for n in range(150)]
rates = [0.005] * 150

for date, rate in zip(dates, rates):
    index_6M.addFixing(date, rate)


swap = VanillaSwap(VanillaSwap.Receiver, notional,
                   fixed_schedule, fixed_rate, Thirty360(),
                   floating_schedule, index_6M, 0.0, Actual360())

swap.setPricingEngine(swap_engine)

price = swap.NPV()

问题根源

错误源于构建Euribor曲线的6个月期限SwapRateHelper参数不匹配:

  • 6个月掉期的固定端付息频率设置为Annual,与浮动端6个月频率冲突,导致现金流时间点逻辑矛盾
  • 使用PiecewiseLogCubicDiscount构建预测曲线,短端插值易出现数值稳定性问题,bootstrap时无法找到有效根区间

修复方案

1. 修正6个月SwapRateHelper的付息频率

将6个月期限的SwapRateHelper固定端付息频率改为Semiannual,匹配浮动端周期:

helpers = [SwapRateHelper(QuoteHandle(SimpleQuote(rate / 100)),
                          Period(int(tenor), Months),
                          TARGET(),
                          Semiannual,  # 此处从Annual改为Semiannual
                          Unadjusted,
                          Thirty360(),
                          Euribor6M(YieldTermStructureHandle(discount_curve)),
                          QuoteHandle(),
                          Period(0, Days),
                          YieldTermStructureHandle(discount_curve))
           for rate, tenor in zip(euribor_short['Rate'], euribor_short['Tenor'])]

2. 更换曲线构建方法

将Euribor曲线的构建方法改为PiecewiseLinearZero,提升短端bootstrap的稳定性:

euribor_curve = PiecewiseLinearZero(0, TARGET(), helpers, Actual365Fixed())

3. 统一日计数规则

确保SwapRateHelper中的固定端日计数与实际掉期一致,同时初始化Euribor6M时传入贴现曲线,避免默认曲线冲突。

修正后的完整代码

from QuantLib import *
import pandas as pd

today = Date(30, September, 2022)
Settings.instance().evaluationDate = today

def create_ql_curve():
    estr_short = pd.DataFrame({'Tenor': [6], 'Rate': [1.7466]})
    estr_long = pd.DataFrame({'Tenor': [1,2,5,10,15,20], 'Rate': [2.2828, 2.547, 2.661, 2.804, 2.85, 2.35]})
    euribor_short = pd.DataFrame({'Tenor': [6], 'Rate': [1.7466]})
    euribor_long = pd.DataFrame({'Tenor': [1,2,5,10,15,20], 'Rate': [2.2828, 2.547, 2.661, 2.804, 2.85, 2.35]})

    ois_helpers = [OISRateHelper(0,
                                 Period(int(tenor), Months),
                                 QuoteHandle(SimpleQuote(rate / 100)),
                                 Eonia(),
                                 YieldTermStructureHandle(),
                                 True)
                   for rate, tenor in zip(estr_short['Rate'], estr_short['Tenor'])]

    ois_helpers += [OISRateHelper(0,
                                  Period(int(tenor), Years),
                                  QuoteHandle(SimpleQuote(rate / 100)),
                                  Eonia(),
                                  YieldTermStructureHandle(),
                                  True)
                    for rate, tenor in zip(estr_long['Rate'], estr_long['Tenor'])]

    discount_curve = PiecewiseLogCubicDiscount(0, TARGET(), ois_helpers, Actual365Fixed())
    discount_curve.enableExtrapolation()

    # 修正6个月SwapRateHelper的付息频率为Semiannual
    helpers = [SwapRateHelper(QuoteHandle(SimpleQuote(rate / 100)),
                              Period(int(tenor), Months),
                              TARGET(),
                              Semiannual,
                              Unadjusted,
                              Thirty360(),
                              Euribor6M(YieldTermStructureHandle(discount_curve)),
                              QuoteHandle(),
                              Period(0, Days),
                              YieldTermStructureHandle(discount_curve))
               for rate, tenor in zip(euribor_short['Rate'], euribor_short['Tenor'])]

    helpers += [SwapRateHelper(QuoteHandle(SimpleQuote(rate / 100)),
                               Period(int(tenor), Years),
                               TARGET(),
                               Annual,
                               Unadjusted,
                               Thirty360(),
                               Euribor6M(YieldTermStructureHandle(discount_curve)),
                               QuoteHandle(),
                               Period(0, Days),
                               YieldTermStructureHandle(discount_curve))
                for rate, tenor in zip(euribor_long['Rate'], euribor_long['Tenor'])]

    # 更换为PiecewiseLinearZero构建Euribor曲线
    euribor_curve = PiecewiseLinearZero(0, TARGET(), helpers, Actual365Fixed())
    euribor_curve.enableExtrapolation()

    return discount_curve, euribor_curve

discount_curve, euribor_curve = create_ql_curve()

forecast_handle = RelinkableYieldTermStructureHandle(euribor_curve)
discount_handle = RelinkableYieldTermStructureHandle(discount_curve)

index_6M = Euribor6M(forecast_handle)
swap_engine = DiscountingSwapEngine(discount_handle)

#  swaps

start_date = Date(23, April, 2014)
maturity_date = Date(23, April, 2023)
fixed_schedule = Schedule(start_date, maturity_date,
                          Period(1, Years), TARGET(), Unadjusted, Unadjusted,
                          DateGeneration.Forward, False)

floating_schedule = Schedule(start_date, maturity_date,
                             Period(6, Months), TARGET(), ModifiedFollowing, ModifiedFollowing,
                             DateGeneration.Forward, True)

notional = 115000000
fixed_rate = 0.01727
fixed_leg_daycount = Actual360()
float_spread = 0
float_leg_daycount = Actual360()

calendar = TARGET()
previous = calendar.advance(today, -27, Weeks)
dates = [calendar.advance(previous, n, Days) for n in range(150)]
rates = [0.005] * 150

for date, rate in zip(dates, rates):
    index_6M.addFixing(date, rate)


swap = VanillaSwap(VanillaSwap.Receiver, notional,
                   fixed_schedule, fixed_rate, Thirty360(),
                   floating_schedule, index_6M, 0.0, Actual360())

swap.setPricingEngine(swap_engine)

price = swap.NPV()
print(f"Swap NPV: {price}")

内容的提问来源于stack exchange,提问作者Oamriotn

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 18:15:38