pythonfinancequantlibquantlib-swig

Using QuantLib to compute cash flows for FloatingRateBond with Floor


Very new to QuantLib so guessing this is a rookie mistake. Enjoyed getting to know this powerful library so thank you to the authors and contributors!

I'm able to generate amounts for cashflows for a FloatingRateBond without a pricer if there isn't a floor argument, so I don't understand why including a floor argument would necessitate a pricer. I would think the addition of the floor would just provide a min for each of the fixing values.

Wanted to see if anyone has gotten the FloatingRateBond cashflows to work while using a floor. And, if so, if anyone can spot where I'm going astray. Thanks in advance!

I'm using the QuantLib 1.8 on windows installed via the pre-packaged installer (QuantLib-Python-1.8.win-amd64-py3.5.msi).

Here's where the error occurs:

File "C:/src/misc/generate_cashflows.py", line 138, in generate_cashflow
    print(cf.amount())
  File "C:\lib\site-packages\QuantLib\QuantLib.py", line 8844, in amount
    return _QuantLib.CashFlow_amount(self)
RuntimeError: pricer not set

The specific code is below:

ql_first_day, ql_first_month, ql_first_year = first_payment_date.day, first_payment_date.month, first_payment_date.year

ql_first_date = QuantLib.Date(ql_first_day, ql_first_month, ql_first_year)  

maturity_month, maturity_day, maturity_year = maturity.month, maturity.day, maturity.year
ql_maturity_date = QuantLib.Date(maturity_day, maturity_month, maturity_year)



ql_issue_day, ql_issue_month, ql_issue_year = issue_date.day, issue_date.month, issue_date.year
q1_settle_date = QuantLib.Date(ql_issue_day, ql_issue_month, ql_issue_year)

fixing_days = 0

calendar = QuantLib.UnitedStates()
ql_settle_date = calendar.adjust(q1_settle_date)
todays_date = calendar.advance(ql_settle_date, -fixing_days, QuantLib.Days)
QuantLib.Settings.instance().evaluationDate = todays_date

ql_schedule = QuantLib.Schedule(ql_settle_date,
                    ql_maturity_date, QuantLib.Period(ql_frequency_enum),
                    QuantLib.UnitedStates(),
                    QuantLib.Following, QuantLib.Following,
                    QuantLib.DateGeneration.Forward, False, ql_first_date)

ql_forecast_curve = QuantLib.RelinkableYieldTermStructureHandle()
today = datetime.datetime.today()
calc_date = QuantLib.Date(today.day, today.month, today.year)
QuantLib.Settings.instance().evaluationDate = calc_date

day_count = QuantLib.Thirty360()

# setup swaps
calendar = QuantLib.UnitedStates()
swFixedLegFrequency = QuantLib.Annual
swFixedLegConvention = QuantLib.Unadjusted
swFixedLegDayCounter = QuantLib.Thirty360()
swFloatingLegIndex = QuantLib.USDLibor(QuantLib.Period(3, QuantLib.Months))

swap_raw = [
    (1, 0.01251),
    (2, 0.01505),
    (3, 0.01701),
    (5, 0.01972),
    (7, 0.02158)
]

swap_rates = []

for year, rate in swap_raw:
    swap_rates.append(QuantLib.SwapRateHelper(
        QuantLib.QuoteHandle(QuantLib.SimpleQuote(rate)),
        QuantLib.Period(year, QuantLib.Years),
        calendar,
        swFixedLegFrequency,
        swFixedLegConvention,
        swFixedLegDayCounter,
        swFloatingLegIndex
    ))

swap_curve = QuantLib.PiecewiseFlatForward(calc_date, swap_rates, day_count)

ql_forecast_curve.linkTo(swap_curve)

ql_index = QuantLib.USDLibor(period, ql_forecast_curve)

settlement_days = 0
face_amount = 100

ql_bond = QuantLib.FloatingRateBond(settlement_days, #settlementDays
    face_amount, # faceAmount
    ql_schedule,
    ql_index,
    QuantLib.Thirty360(),
    gearings = [],
    spreads = [libor_spread],
    caps = [],
    floors = [.01]
)

ql_discount_curve = QuantLib.RelinkableYieldTermStructureHandle()
settlement_date = QuantLib.Date(9, 2, 2017)
flatForward = QuantLib.FlatForward(
    settlement_date,
    .02,
    QuantLib.ActualActual(QuantLib.ActualActual.Bond),
    QuantLib.Compounded,
    QuantLib.Semiannual)
ql_discount_curve.linkTo(flatForward)
bondEngine = QuantLib.DiscountingBondEngine(ql_discount_curve)
ql_bond.setPricingEngine(bondEngine)

for cf in ql_bond.cashflows():
    c = QuantLib.as_floating_rate_coupon(cf)
    print(cf.amount())

Solution

  • The theory first: when pricing the coupon with a floor, you can't just take the expected LIBOR rate from your forecast curve and take the minimum between that and the floor. Instead, you need to take the expected value of the minimum between the rate and the floor, and unfortunately E[min(R,F)] is not the same as min(E[R],F). So no, the floor doesn't just provide a minimum; you need a different formula to estimate the expected payoff.

    The implication for QuantLib is that simple floating-rate coupons can be (and are) set a default pricer that just reads the rate off the forecast curve, but coupons with caps or floors need the user to specify what pricer to use and to provide it with any additional needed data; in your case, this means at least a volatility term structure, although more optional data can be specified; see the constructor of the BlackIborCouponPricer class for details.

    Usually, the volatility is bootstrapped on market quotes for caps and floors, but the procedure to create it is kind of complex (see these tests for an example in C++), I'm not sure that all the needed classes are exported to Python, and you'll be better off asking about it on the QuantLib mailing list.

    If you want to verify that the coupons can works, you can use a constant volatility, as in:

    volatility = 0.10;
    vol = QuantLib.ConstantOptionletVolatility(settlement_days,
                                               calendar,
                                               QuantLib.ModifiedFollowing,
                                               volatility,
                                               day_count)
    
    pricer = QuantLib.BlackIborCouponPricer(
        QuantLib.OptionletVolatilityStructureHandle(vol))
    QuantLib.setCouponPricer(ql_bond.cashflows(), pricer)
    

    The above should enable you to get a result; but of course I pulled the 15% volatility out of a hat, and it won't give you actual market values...