QuantLib icon indicating copy to clipboard operation
QuantLib copied to clipboard

QL-ORE ON coupons alignment

Open paolodelia99 opened this issue 4 months ago • 19 comments

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)

paolodelia99 avatar Aug 23 '25 16:08 paolodelia99

Coverage Status

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

coveralls avatar Aug 23 '25 17:08 coveralls

@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?

paolodelia99 avatar Aug 27 '25 11:08 paolodelia99

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 avatar Aug 27 '25 12:08 lballabio

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

I still have to do a proper review...

lballabio avatar Sep 03 '25 10:09 lballabio

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

paolodelia99 avatar Sep 03 '25 11:09 paolodelia99

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

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

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

What do you think?

lballabio avatar Sep 26 '25 07:09 lballabio

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?

paolodelia99 avatar Sep 27 '25 15:09 paolodelia99

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.

lballabio avatar Sep 29 '25 06:09 lballabio

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 avatar Sep 29 '25 08:09 paolodelia99

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?

paolodelia99 avatar Sep 29 '25 15:09 paolodelia99

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.

lballabio avatar Sep 29 '25 16:09 lballabio

@pcaspers no hurry, just bumping this in case it fell to the bottom of your inbox and got lost, we had a few questions for you above. Thanks!

lballabio avatar Nov 10 '25 11:11 lballabio

I will take a look - I have flagged it in my mailbox :-)

pcaspers avatar Nov 10 '25 12:11 pcaspers

Thanks for doing all this. @paolodelia99 do you plan to reintegrate the updated ql coupon classes in ore, so that we can get rid of the QuantExt versions of these classes?

pcaspers avatar Nov 12 '25 07:11 pcaspers

Do mean in the ORE's QL fork? @pcaspers But yeah, it shoudn't be that big of a problem to integrate those changes in ORE.

paolodelia99 avatar Nov 12 '25 13:11 paolodelia99

Do mean in the ORE's QL fork? @pcaspers But yeah, it shoudn't be that big of a problem to integrate those changes in ORE.

Ultimately yes. Maybe it is enough if you migrate the code to official ql such that we can merge this later into our ql fork and get rid of the QuantExt classes.

pcaspers avatar Nov 14 '25 09:11 pcaspers

@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);
}

paolodelia99 avatar Nov 26 '25 22:11 paolodelia99

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)?

lballabio avatar Nov 27 '25 15:11 lballabio

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 avatar Nov 27 '25 16:11 paolodelia99

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

paolodelia99 avatar Dec 08 '25 20:12 paolodelia99

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

lballabio avatar Dec 15 '25 15:12 lballabio

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.

lballabio avatar Dec 16 '25 10:12 lballabio

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

pcaspers avatar Dec 16 '25 10:12 pcaspers

Sure, thanks.

lballabio avatar Dec 16 '25 10:12 lballabio

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());

paolodelia99 avatar Dec 16 '25 16:12 paolodelia99

Sounds good - did you try that?

pcaspers avatar Dec 16 '25 16:12 pcaspers

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?

paolodelia99 avatar Dec 16 '25 18:12 paolodelia99

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

pcaspers avatar Dec 16 '25 18:12 pcaspers

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.

lballabio avatar Dec 17 '25 10:12 lballabio