Skip to content

Conversation

@paolodelia99
Copy link
Contributor

@paolodelia99 paolodelia99 commented Aug 23, 2025

This PR tries to be an alignment for the difference of the overnight coupons and ON coupons pricers in QuantLib and ORE.

Changes to note for ORE (@pcaspers ):

  • the OvernightIndexedCouponPricer code is in the overnightindexedcouponpricer.hpp file not in the overnightindexedcoupon.hpp file anymore.
  • rateCutoff in ORE is lockoutDays in QuantLib
  • fixingDays in ORE is lookbackDays in QuantLib
  • there is no AverageONIndexedCoupon in QuantLib the averaging method is passed as a argument in the OvernightIndexedCoupon and it is an enum type:
 struct RateAveraging {
        enum Type {
            Simple, 
            Compound 
        };
    };
  • The QL OvernightIndexedCouponPricers pricing logic haven't been changes since I haven't noticed substantial differences (apart from the rateCutoff and fixingDays naming conventions). The only thing that I'm unsure of is the following line. I haven't seen such thing in QuantLib.

Changes to note in QL (@lballabio ):

  • Added CappedFlooredOvernightIndexedCoupon class under overnigthindexcoupon.hpp (imported from ORE)
  • Added BlackOvernightIndexedCouponPricer and BlackAverageONIndexedCouponPricer for pricing capped / floored compounded ON coupons (imported from ORE)
  • Added a bunch of other methods in the OvernightLeg class (includeSpread, withCaps, withFloors, withNakedOption, ...) imported from ORE
  • Added includeSpread, rateComputationStartDate, rateComputationEndDate arguments to the OvernightIndexedCoupon constructor (missing args from ORE)

@coveralls
Copy link

coveralls commented Aug 23, 2025

Coverage Status

coverage: 74.162% (+0.3%) from 73.873%
when pulling 5096ee8 on paolodelia99:feature/ql-ore-coupons-alignment
into 7a6a9a2 on lballabio:master.

@paolodelia99
Copy link
Contributor Author

@lballabio I actually need help with the test testOvernightLegWithCapsAndFloors in overnightindexedcoupon.hpp. Don't know why I'm getting different results on different builds, at first I thought that I was due to the usingAtParCoupons optional, but apparently I was wrong. Can you please take a look at it when you have time?

@lballabio
Copy link
Owner

Not a lot of time now unfortunately, but I would try checking that we're not accessing some vector out of its bounds and getting garbage numbers.

@lballabio
Copy link
Owner

I pushed a fix for the failing tests—a couple of bools were left uninitialized.

I still have to do a proper review...

@paolodelia99
Copy link
Contributor Author

Thank you @lballabio! I didn't have much time to debug the tests lately

@paolodelia99 paolodelia99 marked this pull request as ready for review September 6, 2025 10:30
@lballabio
Copy link
Owner

Apologies for the long delay. One thing: currently the pricers are defined this way:

classDiagram

class FloatingRateCouponPricer
class CompoundingOvernightIndexedCouponPricer
class ArithmeticAveragedOvernightIndexedCouponPricer
class CappedFlooredOvernightIndexedCouponPricer
class BlackOvernightIndexedCouponPricer
class BlackAverageONIndexedCouponPricer

FloatingRateCouponPricer <|-- CompoundingOvernightIndexedCouponPricer
FloatingRateCouponPricer <|-- ArithmeticAveragedOvernightIndexedCouponPricer
FloatingRateCouponPricer <|-- CappedFlooredOvernightIndexedCouponPricer
CappedFlooredOvernightIndexedCouponPricer <|-- BlackOvernightIndexedCouponPricer
CappedFlooredOvernightIndexedCouponPricer <|-- BlackAverageONIndexedCouponPricer
Loading

Instead, I would try to turn it into

classDiagram

class FloatingRateCouponPricer
class CompoundingOvernightIndexedCouponPricer
class ArithmeticAveragedOvernightIndexedCouponPricer
class BlackOvernightIndexedCouponPricer
class BlackAverageONIndexedCouponPricer

FloatingRateCouponPricer <|-- CompoundingOvernightIndexedCouponPricer
FloatingRateCouponPricer <|-- ArithmeticAveragedOvernightIndexedCouponPricer
CompoundingOvernightIndexedCouponPricer <|-- BlackOvernightIndexedCouponPricer
ArithmeticAveragedOvernightIndexedCouponPricer <|-- BlackAverageONIndexedCouponPricer
Loading

or maybe, if we need to share something between compounded and averaged, something like

classDiagram

class FloatingRateCouponPricer
class OvernightIndexedCouponPricer
class CompoundingOvernightIndexedCouponPricer
class ArithmeticAveragedOvernightIndexedCouponPricer
class BlackOvernightIndexedCouponPricer
class BlackAverageONIndexedCouponPricer

FloatingRateCouponPricer <|-- OvernightIndexedCouponPricer
OvernightIndexedCouponPricer <|-- CompoundingOvernightIndexedCouponPricer
OvernightIndexedCouponPricer <|-- ArithmeticAveragedOvernightIndexedCouponPricer
CompoundingOvernightIndexedCouponPricer <|-- BlackOvernightIndexedCouponPricer
ArithmeticAveragedOvernightIndexedCouponPricer <|-- BlackAverageONIndexedCouponPricer
Loading

What do you think?

@paolodelia99
Copy link
Contributor Author

2nd and 3rd case make more sense to me. I would opt for the 3rd case. One question: is OvernightIndexedCouponPricer gonna be a virtual class that is just defining the interface for the child classes?

@lballabio
Copy link
Owner

The interface is already declared in FloatingRateCouponPricer. The base class might perhaps store the volatility term structure, but otherwise there's not a lot in common, which is why the existing pricers don't have a common base.

@paolodelia99
Copy link
Contributor Author

Got it. So the OvernightIndexedCoupon pricer class should do the same thing as the IborCouponPricer class, where the volatility term structure is set by the setCapletVolatility method. In this way, we are going to have a consistent interface among those "base classes".

@paolodelia99
Copy link
Contributor Author

One problem that I have noticed @lballabio: If BlackOvernightIndexedCouponPricer is going to become a child of CompoundingOvernightIndexedCouponPricer there is going to be a clash in the type of coupon_ attribute, since the the former is a CappedFlooredOvernightIndexedCoupon and in the latter is OvernightIndexedCoupon (They are both child of FloatingRateCoupon). How would you suggest me to tackle this problem?

@lballabio
Copy link
Owner

I think you can copy the approach in CappedFlooredIborCoupon. It inherits from CappedFlooredCoupon, not FloatingRateCoupon directly, and the setPricer method in the base class is overridden so that it sets the passed pricer to its underlying instead of the capped coupon itself. It should work in this case as well. Let me know if you need more details.

@paolodelia99
Copy link
Contributor Author

@lballabio don't know is it better to set lookbackDays = 0 in the OvernightIndexedCoupon ctor instead of Null<Natural>(), because If I had to use ORE's logic (ORE is using a Period instead of a Natural) in the conditional that I've just imported I would have a bug:

if (lookbackDays != 0) {  //by default lookbackDays is 2147483647
            BusinessDayConvention bdc = lookbackDays > 0 ? Preceding : Following;
            valueStart = overnightIndex->fixingCalendar().advance(valueStart, -lookbackDays, Days, bdc); //error is throw in here: QuantLib::Error: Date's serial number (366) outside allowed range [367-109574]
            valueEnd = overnightIndex->fixingCalendar().advance(valueEnd, -lookbackDays, Days, bdc);
}

@lballabio
Copy link
Owner

Hi Paolo, I'm not sure I understand the question. Right now you have if (lookbackDays != Null<Natural>()) which is correct. Why should it become if (lookbackDays != 0)?

@paolodelia99
Copy link
Contributor Author

I was just wondering whether to set lookbackDays = Null<Natural>() was the correct approach instead of setting it to 0. Anyway the initial check is correct, so there is no need to change it. Just asking for consistency reasons since lockoutDays = 0 by the ctor, by default

@paolodelia99
Copy link
Contributor Author

I fixed the issues you guys pointed out. Whenever you have time to review it @lballabio @pcaspers

@lballabio
Copy link
Owner

Thanks, I'm looking at it now so hopefully it goes into 1.41.

Comment on lines 241 to 243
OvernightLeg& withOvernightIndexedCouponPricer(const ext::shared_ptr<FloatingRateCouponPricer>& couponPricer);
OvernightLeg& withCapFlooredOvernightIndexedCouponPricer(
const ext::shared_ptr<OvernightIndexedCouponPricer>& couponPricer);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pcaspers as I see it, there's no reason to have two different methods to pass two different pricers; the Black pricer can manage coupons both with and without caps/floors. Do you have any use cases that say otherwise?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lballabio no, you are right, we don't need both methods

@lballabio
Copy link
Owner

Hmm, there seems to be a problem that didn't surface until I tried to use just one pricer for both capped and non-capped coupons. If one sets a Black coupon pricer to an OvernightIndexedCoupon (not the capped one), this triggers an infinite recursion. BlackCompoundingOvernightIndexedCouponPricer::initialize calls coupon_->effectiveIndexFixing(), which calls back pricer.initialize(). I'll look into it a bit more. If you have any ideas, they're welcome.

@pcaspers
Copy link
Contributor

You can leave it to me if you want, just give me a couple of days

@lballabio
Copy link
Owner

Sure, thanks.

@paolodelia99
Copy link
Contributor Author

I figured out what the error is due to: in ORE the BlackCompoundingOvernightIndexedCouponPricer::initialize method is calling

effectiveIndexFixing_ = coupon_->underlying()->effectiveIndexFixing();

but since now that we also need to be able to price an OvernigthIndexedCoupon with that class we the initialize method is calling:

effectiveIndexFixing_ = coupon_->effectiveIndexFixing();

thus leading to that infinite recursion.
I think we should call the compute method defined in the CompoundingOvernightIndexedCouponPricer parent class, since the BlackCompoundingOvernightIndexedCouponPricer does calculate only the caplet/floorlet rate.

auto [swapletRate, effectiveSpread, effectiveIndexFixing] = compute(coupon_->accrualEndDate());

@pcaspers
Copy link
Contributor

Sounds good - did you try that?

@paolodelia99
Copy link
Contributor Author

Yeah, it worked. This is the code that I've added:

auto [swapletRate, effectiveSpread, effectiveIndexFixing] = CompoundingOvernightIndexedCouponPricer::compute(coupon_->accrualEndDate());
swapletRate_ = swapletRate;
effectiveSpread_ = effectiveSpread;
effectiveIndexFixing_ = effectiveIndexFixing;

Are we sure that we want to trigger the CompoundingOvernightIndexedCouponPricer::compute in the initialize method in the BlackCompoundingOvernightIndexedCouponPricer?

@pcaspers
Copy link
Contributor

makes sense to me, we need these results for the underlying coupon, so why not set them here

@lballabio
Copy link
Owner

Thanks! Another thing turned up: I added a few more checks in the tests, and one of them fails because setting a Black arithmetic-averaging pricer to a vanilla coupon results in a rate = 0 being returned. I'm pushing it so you can have a look.

vanillaCoupon->setPricer(pricer);

Rate rate = vanillaCoupon->rate();
Rate expectedRate = 0.039765917;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this rate supposed to be the same as in the Compounding averaging case? I get a different value with the ArithmeticAveragedOvernightIndexedCouponPricer

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to call the ArithmeticAveragedOvernightIndexedCouponPricer::swaplet() method in the BlackAveragingOvernightIndexedCouponPricer::initialize method, just like we did with the BlackCompoundingOvernightIndexedCouponPricer case since aslo in the ORE code the swapletRate_ was set to

swapletRate_ = coupon_->underlying()->rate();

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's not supposed to be the same.

@lballabio
Copy link
Owner

Great, thanks.

Thanks to you both for the effort on this one!

@lballabio lballabio added this to the Release 1.41 milestone Dec 18, 2025
@lballabio lballabio merged commit c15a6fe into lballabio:master Dec 18, 2025
48 of 49 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants