-
Notifications
You must be signed in to change notification settings - Fork 2k
Feature: Fast Valuation of Seasoned OIS Swaps #2213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5f7acde
e25c32c
076f8d4
1ba2f50
b20080a
aff982f
70cacfe
ffad4a7
0ca6ff0
0a4e976
6c150d0
02d7fde
ede4091
4b45d72
2b52546
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ | |
|
||
#include <ql/indexes/iborindex.hpp> | ||
#include <ql/termstructures/yieldtermstructure.hpp> | ||
#include <ql/time/schedule.hpp> | ||
#include <utility> | ||
|
||
namespace QuantLib { | ||
|
@@ -60,16 +61,15 @@ | |
|
||
ext::shared_ptr<IborIndex> IborIndex::clone( | ||
const Handle<YieldTermStructure>& h) const { | ||
return ext::make_shared<IborIndex>( | ||
familyName(), | ||
tenor(), | ||
fixingDays(), | ||
currency(), | ||
fixingCalendar(), | ||
businessDayConvention(), | ||
endOfMonth(), | ||
dayCounter(), | ||
h); | ||
return ext::make_shared<IborIndex>(familyName(), | ||
tenor(), | ||
fixingDays(), | ||
currency(), | ||
fixingCalendar(), | ||
businessDayConvention(), | ||
endOfMonth(), | ||
dayCounter(), | ||
h); | ||
} | ||
|
||
|
||
|
@@ -82,6 +82,84 @@ | |
: IborIndex(familyName, 1*Days, settlementDays, curr, | ||
fixCal, Following, false, dc, h) {} | ||
|
||
void OvernightIndex::addFixing(const Date& fixingDate, Real fixing, bool forceOverwrite) { | ||
calculated_ = false; | ||
Index::addFixing(fixingDate, fixing, forceOverwrite); | ||
} | ||
|
||
void OvernightIndex::addFixings(const TimeSeries<Real>& t, bool forceOverwrite) { | ||
calculated_ = false; | ||
Index::addFixings(t, forceOverwrite); | ||
} | ||
|
||
Rate OvernightIndex::compoundedFixings(const Date& fromFixingDate, const Date& toFixingDate) { | ||
calculate(); | ||
auto yearFraction = dayCounter_.yearFraction(fromFixingDate, toFixingDate); | ||
auto compIndexEnd = compoundIndex_[toFixingDate]; | ||
auto compIndexStart = compoundIndex_[fromFixingDate]; | ||
|
||
if (compIndexStart == Null<Real>() || compIndexEnd == Null<Real>()) | ||
return Null<Real>(); | ||
|
||
return (compoundIndex_[toFixingDate] / compoundIndex_[fromFixingDate] - 1) / yearFraction; | ||
} | ||
|
||
Real OvernightIndex::compoundedFactor(const Date& fromFixingDate, const Date& toFixingDate) { | ||
calculate(); | ||
auto compIndexEnd = compoundIndex_[toFixingDate]; | ||
auto compIndexStart = compoundIndex_[fromFixingDate]; | ||
|
||
if (compIndexStart == Null<Real>() || compIndexEnd == Null<Real>()) | ||
return Null<Real>(); | ||
|
||
return compIndexEnd / compIndexStart; | ||
} | ||
|
||
void OvernightIndex::performCalculations() const { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure LazyObject is the right mechanism for this. This calculation will be executed again and again every time the index (or the pricer, if you move the calculation there) receives a notification; for instance, when the forecast curve changes, even though it has no effect on the stored past prices on which this calculation depends. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will the calculation be triggered also when the forecast curve changes as well? Not just when I call the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I see, because you've removed the part in |
||
auto getLastFixingDate = [](Calendar fixingCalendar, std::vector<Date>& fixingDates){ | ||
Date lastFixingDay = fixingDates.front(); | ||
std::for_each(fixingDates.begin() + 1, fixingDates.end(), [&](Date& fixingDate){ | ||
if (fixingCalendar.advance(lastFixingDay, Period(1, Days)) == fixingDate) | ||
lastFixingDay = fixingDate; | ||
}); | ||
return lastFixingDay; | ||
}; | ||
|
||
const auto& ts = Index::timeSeries(); | ||
auto fixingDates = ts.dates(); | ||
if (fixingDates.empty()) { | ||
compoundIndex_ = TimeSeries<Real>(); | ||
return; | ||
} | ||
|
||
auto fixingCalendar = InterestRateIndex::fixingCalendar(); | ||
auto lastFixingDate = getLastFixingDate(fixingCalendar, fixingDates); | ||
Schedule schedule = MakeSchedule() | ||
.from(fixingDates.front()) | ||
.to(lastFixingDate) | ||
Comment on lines
+138
to
+139
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using performCalculations(), which has no arguments, forced you to calculate compounded values for the whole history, but that might be unnecessary. If possible, I'd calculate the series of compounded values only on the requested range, extending it as needed with each call. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know it's inefficient. Ideally, assuming that one should add fixings from the last fixing day that we have in history in the compounded index, the optimization that can be put in place could be to compute the compounded index from the last past fixing date to the new past date of the next fixing. |
||
.withTenor(Period(1, Days)) | ||
.withCalendar(fixingCalendar) | ||
.withConvention(Following) | ||
.withTerminationDateConvention(Following); | ||
std::vector<Real> compIndexValues(schedule.size()); | ||
Real lastCompIndexValue = 1.000; | ||
compIndexValues[0] = lastCompIndexValue; | ||
|
||
std::transform(schedule.begin(), schedule.end() - 1, schedule.begin() + 1, compIndexValues.begin() + 1, | ||
[&](const Date& d1, const Date& d2) { | ||
lastCompIndexValue *= (1 + ts[d1] * dayCounter_.yearFraction(d1, d2)); | ||
return lastCompIndexValue; | ||
}); | ||
|
||
compoundIndex_ = TimeSeries<Real>(schedule.begin(), | ||
schedule.end(), | ||
compIndexValues.begin()); | ||
} | ||
|
||
void OvernightIndex::update() { | ||
notifyObservers(); | ||
} | ||
|
||
ext::shared_ptr<IborIndex> OvernightIndex::clone( | ||
const Handle<YieldTermStructure>& h) const { | ||
return ext::shared_ptr<IborIndex>( | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this belongs into the index. It should be in the coupon or the coupon pricer—especially because the way the composition is done doesn't depend only on the start and end date, but also on the lookback days and whether or not one must apply the observation shift, which are parameters of the coupon.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my opinion, the compounded index should be linked to the Overnight Index itself, unless we are using the same CompoundedOvernightIndexPricer within the same
Leg
. I don’t see any performance improvements from having a different instance of theCompoundedOvernightIndexPricer
for each coupon, especially if each has an associated compoundedIndex attribute; each pricer is going to recalculate those compounded fixings in the past, not leveraging the compounded index.Aren't the lookback days and the observation shift logics that concern the Coupon itself? I feel like they shouldn't concern the compounded index. I don't really see why the compounded index should have taken into account that logic, since we could have coupons on the same Overnight index with different lookback days, and so on.
Another practical way to store the compoundedIndex could be by utilizing a sort of
IndexManager
, similar to what you have implemented with the standard fixings.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lookback days and the observation shift concern the Coupon, and they have an effect on the compounding. As an example, let's consider a coupon starting today, Monday May 12th, with lookback days = 2, so we have for instance these initial compounding dates:
If the observation shift is set to false, the coupon dates in the first column determine the compounding days; so for instance, the index fixing for Friday May 9th is compounded for 1 day (May 13th to May 14th) and the fixing for Wednesday May 14th is compounded for 3 days (May 16th to May 19th). If the observation shift is set to true, it's the fixing dates in the second column that determine the compounding days, so the index fixing for Friday May 9th is compounded for 3 days and the fixing for Wednesday May 14th is compounded for 1 day. A different number of lookback days would give you other results.
The index alone can't know this. It can only give you single fixings. It's the coupon that should manage this calculation.
--
About having different instances of the coupon pricer: yes, the cache should be shared between pricers, like the IndexManager.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see what you're saying, and I agree that it’s ultimately the coupon that should handle the compounding, not the compounded index itself. The compounded index can give us the individual fixings (since that’s what it essentially stores), but the logic for compounding over a period is typically managed within the coupon itself.
How would you suggest to tackle this problem then? I was thinking to resister and additional index in in the
IndexManager
(with the name "<Overnight_index> Comp"), and store the compounded index there since now if I feel like it's the natural place to store it. But again the problems will come from the fact that we might have non-contiguous fixing (calendar-wise I mean), so we could have the comp index for a period (like from the 1st of Janurary to the 31st of Janurary) but then let's say we are missing some fixings (first week of February) and we have the fixing after those. How should we deal with that situation?My idea is to not store anything and then fallback on normal fixing (stored in the normal index).
Example of CompIndex in
IndexManager
(assuming that the available fixings start from the 1st of January 2025)Name: "Estr Comp"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there are fixings for a given week, we should also have the compounded fixings, shouldn't we? In what case do you think we might end up having holes in the compounded series?
I'm not sure if IndexManager is the correct place, because there's not just one compounded series for a given index. The compounded series for 2 lookback days and observation shift is different from the compounded series for 2 lookback days and no observation shift, which is also different from the compounded series for 3 lookback days and no observation shift. You could try to put all of that into a tag, but you might also think about having a dedicated class to store them—I'm not sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes we should have a bijective relationship between the fixings and compounded fixings.
We might end up with holes, in case the user will miss a period (intentionally or unitentionally) in the fixings. Take for example the fixings that we have in
CommonVars
struct undertest-suite\overnightindexedcoupon.cpp
. With the logic that I have implemented the compoundedIndex will be calculated from the 21st of June 2019 to the 5th of August 2019, ignoring the fixings from the 18th of October 2021 to the 22nd of November 2021.With logic that I've implemented right now, we are calculating the average rate for the second period, it is going to fallback on using the normal fixings to calculate the averageRate (sequentially multiplying the fixings), not leveraging the compounded index (where we just to do a division between to elements of the compoundedIndex).
The compoundedIndex I meant is an alternate way of storing an overnightIndex plain fixings; beyond standard fixings central bank are also publishing the corresponding compoundedIndexes (SOFR Index, Compounded euro short-term rate index, CORRA Index, ...)
I agree that for different coupouns we need different compoundFactors, depending on settings like observation shift and lookback days. That said, those adjustments do not apply to the plain
compoundedIndex
, which represents the raw compounded values without any such transformations. In case of observationShift, I would keep things are they are right now, hence let the coupound pricer manage all the logic (even though it is possible to get back normal fixings from the compounded index). However, in the default case (no observation shift or lookback), I would use the stored compoundedIndex directly for performance and consistency.Let me know what do think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately for most traded instruments the default is to have lookback days. We would almost always fall back to multiplying, which would make this a lot of effort for little gain...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get it, too much effort for a little gain.
Can I keep the refactoring that I've done in the
averageRate
code (inovernightindexedcouponpricer.cpp
)? I felt like the function was too big and not so understandable (Maybe it's better to open another PR for that)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not against separating the three parts; but I think that, when reading the new
averageRate
, it's not very clear (for instance) that they takei
andcompoundFactor
and update them. Maybe they could return the updated values? Also, they take a lot of parameters, which makes them lose some of the readability they gained. Maybe you could try enhancing the comments instead so they help reading through the function? What do you think?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now, that I look at it, yeah too many parameters, I could just have passed a reference to the coupon. On top of that
i
has to updated as well.I'll see what I can accomplish.