[Python] Pricing of cap in Quantlib python with constant vola and with vola surface comparison

Discussão em 'Python' iniciado por Stack, Junho 17, 2025.

  1. Stack

    Stack Membro Participativo

    I want to price a CAP in Quantlib (in Python) using once a constant vola and once a surface. I have given vola rates for periods 1 year until 5 years. Those are the volas "at the money". But we assume them to be constant for all strikes. My code looks like this:

    import QuantLib as ql
    import numpy as np

    reference_date = ql.Date(25,11,2019)
    ql.Settings.instance().evaluationDate = reference_date

    dates_ois = [ql.Date(25,11,2019), ql.Date(25,11,2020), ql.Date(25,11,2021),
    ql.Date(25,11,2022), ql.Date(25,11,2023), ql.Date(25,11,2024), ql.Date(25,11,2025)]
    rates_ois = [0.02, 0.023, 0.025, 0.028, 0.03, 0.027, 0.025]

    dates_3M = dates_ois
    rates_3M = [0.025, 0.028, 0.03, 0.033, 0.035, 0.032, 0.03]

    calendar = ql.TARGET()
    day_count = ql.Actual360()
    interp = ql.Linear()
    comp = ql.Compounded
    freq = ql.Annual

    curve_ois = ql.ZeroCurve(dates_ois, rates_ois, day_count, calendar, interp, comp, freq)
    curve_ois_handle = ql.YieldTermStructureHandle(curve_ois)

    curve_3M = ql.ZeroCurve(dates_3M, rates_3M, day_count, calendar, interp, comp, freq)
    curve_3M_handle = ql.YieldTermStructureHandle(curve_3M)

    start = reference_date + 2
    end = calendar.advance(start, ql.Period('4Y'))
    schedule = ql.Schedule(start, end, ql.Period('3M'), calendar, ql.ModifiedFollowing,
    ql.ModifiedFollowing, ql.DateGeneration.Forward, False)
    notational = 1000000
    capRate = 0.03

    cap = ql.Cap(ql.IborLeg([notational], schedule, ql.Euribor3M(curve_3M_handle)), [capRate])
    vola = ql.QuoteHandle(ql.SimpleQuote(0.0036))
    engine = ql.BachelierCapFloorEngine(curve_ois_handle, vola)
    cap.setPricingEngine(engine)
    print(cap.NPV())


    Now I try the other way, first defining my vola tenors and values (my 4 year vola is exactly the 0.0036 that I had used in the first version of the code)).

    tenors_vola = [ql.Period('1Y'), ql.Period('2Y'), ql.Period('3Y'), ql.Period('4Y'), ql.Period('5Y')]
    rates_vola = [0.0025, 0.0027, 0.0032, 0.0036, 0.0038]

    strikes = [0,1]
    rates_vola_new = [[item, item] for item in rates_vola]

    capfloor_vols = ql.CapFloorTermVolSurface(2, calendar, ql.ModifiedFollowing, tenors_vola, rates_vola_new)
    optionlet_surf = ql.OptionletStripper1(capfloor_vols, ql.Euribor3M(curve_3M_handle), type=ql.Normal)
    optionlet_vols = ql.StrippedOptionletAdapter(optionlet_surf)
    optionlet_vols_handle = ql.OptionletVolatilityStructureHandle(optionlet_vols)

    engine = ql.BachelierCapFloorEngine(curve_ois_handle, optionlet_vols_handle)
    cap.setPricingEngine(engine)
    print(cap.NPV())


    I was thinking I should be getting the same NPV but that's not the case. The volas I have given for the different tenors are par volas and if I understand correctly the optionlet_vols from the 2nd code should be the "bootstrapped" volas, right? As I said for each tenor I would like to assume a flat vola curve (in the strike dimension) using the "at the money"-vola as a constant for all strikes. I would have thought choosing strike = [0,1] oder strike = [0, k] with k being any other number between 0 and 1 shouldn't make a difference, as QL would interpolate between 0 and 1 and make the vola constant but that is not the case. I am clearly still misunderstanding the concept. So can anyone explain and help me? I would like to reproduce the NPV I am getting from the 1st code by using a constant vola surface in the the 2nd code.

    Continue reading...

Compartilhe esta Página