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.137% (+0.3%) from 73.873%
when pulling 282e742 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.

Copy link
Owner

@lballabio lballabio left a comment

Choose a reason for hiding this comment

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

Ok, another round of comments — sorry for the piecewise review, but it's a massive PR...

Comment on lines +74 to +75
const Date& rateComputationStartDate = Date(),
const Date& rateComputationEndDate = Date());
Copy link
Owner

Choose a reason for hiding this comment

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

Probably more for @pcaspers — these two don't seem to be used anywhere in the calculation. Should they be there at all?

Copy link
Contributor

Choose a reason for hiding this comment

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

@lballabio Apologies for the rather late reply. They can be used to overwrite start and end date

    Date valueStart = rateComputationStartDate_ == Null<Date>() ? startDate : rateComputationStartDate_;
    Date valueEnd = rateComputationEndDate_ == Null<Date>() ? endDate : rateComputationEndDate_;

It's rarely used in practice, but there are exotic variants of on coupons where the compounding takes place over the previous (say) 3m period to get the rate for the current 3m period. We introduced these two fields to model cases like this.

Copy link
Owner

Choose a reason for hiding this comment

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

Yes, I can see that was the intent, but those two fields are never assigned to valueStart or used anywhere in the code, hence the question.

Copy link
Contributor

Choose a reason for hiding this comment

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

We assign them here to valueStart and valueEnd. Sorry, I think I am missing something or we are talking about different branches / files?

Copy link
Owner

Choose a reason for hiding this comment

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

Ok, I see, that bit of code wasn't migrated in this PR, which makes those two variables unused here. @paolodelia99, may you have a look?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I'll take a look at it in the upcoming days. It must have been an oversight of mine.

return spread();

if (averagingMethod_ == RateAveraging::Simple)
QL_FAIL("Average OIS Coupon does not have an effectiveSpread"); // FIXME: better error message
Copy link
Owner

Choose a reason for hiding this comment

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

For @pcaspers — does not have yet, or it can't be defined, or in the simple case is the same as the spread?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hm. I'd say for Simple the effective spread is equal to the spread, as you say. We don't expose a method therefore in our averaged coupon class, but no reason to fail here in my opinion.

Comment on lines 256 to 263
if (!coupon_->includeSpread()) {
finalRate += coupon_->spread();
effectiveSpread = coupon_->spread();
effectiveIndexFixing = finalRate;
} else {
effectiveSpread = finalRate - (compoundFactorWithoutSpread - 1.0) / tau;
effectiveIndexFixing = finalRate - effectiveSpread;
}
Copy link
Owner

Choose a reason for hiding this comment

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

I'm puzzled here — in the if the effective index fixing equals the final rate and therefore inlcudes the spread, but in the else it seems to exclude it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You should ask it to @pcaspers, it took this logic from ORE. But I guess in the first case, where the spread is not included in the compounding logic, the effectiveIndexFixing is set to finalRate because the observable/effective fixing used downstream is the final coupon rate after the additive spread.

Copy link
Owner

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at the code in QuantExt, OvernightIndexedCouponPricer::compute(), the compoundFactor will include the spread component if and only if includeSpread == true. I.e. the same holds for rate (which is called finalRate here I think)

    Rate rate = (compoundFactor - 1.0) / tau;

And for the includeSpread == true case we have to subtract the spread component to get the effective fixing.

Copy link
Owner

Choose a reason for hiding this comment

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

Ok, but for the includeSpread == false case you're setting the effective fixing to the final rate, which includes the spread (it's added just two lines above). Shouldn't the spread be excluded?

Copy link
Contributor

Choose a reason for hiding this comment

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

again maybe we are talking about different branches / files? We set indexFixing to rate here. The spread is added to swapletRate though.

Copy link
Owner

Choose a reason for hiding this comment

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

Same here, I'm looking at the code in this PR which differs from the ORE code.

Copy link
Contributor

Choose a reason for hiding this comment

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

ok then it's a question for @paolodelia99 I guess?

@lballabio
Copy link
Owner

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

@pcaspers
Copy link
Contributor

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

@pcaspers
Copy link
Contributor

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?

@paolodelia99
Copy link
Contributor Author

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.

@pcaspers
Copy link
Contributor

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.

@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

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