Skip to content
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

Make floating-rate coupons lazy #1566

Merged
merged 38 commits into from
May 9, 2023
Merged

Conversation

pcaspers
Copy link
Contributor

No description provided.

@pcaspers
Copy link
Contributor Author

updated version of #1222

@coveralls
Copy link

coveralls commented Jan 23, 2023

Coverage Status

Coverage: 71.986% (+0.03%) from 71.955% when pulling e80b203 on pcaspers:peter_lazy_frcpn into 860d1e3 on lballabio:master.

@lballabio
Copy link
Owner

Some details. The main idea of the PR is to make floating-rate coupons lazy (as in, inherited from LazyObject) for performance reasons. However, this can cause problems, because then we'd have some lazy objects (e.g., bonds and swaps) registering as observers with other lazy objects (the coupons).

The possible problems are triggered as follows:

  • a lazy A registers with a lazy B.
  • B is updated and forwards to A a notification; from that point, it won't forward any further notifications until it's asked to calculate (this is, again, for performance reasons; the rationale is that A assumes B has not recalculated, since B didn't ask A for results).
  • However, A might calculate without asking B for results (for instance, because A is an expired instrument).
  • Now, B might change again, but it is still not forwarding notifications. Hence, A won't know it needs to recalculate and will keep returning the cached results.

This happened in a couple of places in the library, and was fixed with ad-hoc solutions; in CompositeInstrument by using alwaysForwardNotifications and in Swaption by not registering with the underlying swap itself but with its observables.

Possible solutions:

  • Use alwaysForwardNotifications and call it manually in the constructor of the lazy observer (A above); this is the solution used in this PR. This requires changes to a number of classes because we're inserting manual calls when needed. It would also require similar client code to be changed as well.
  • Use alwaysForwardNotifications but call it automatically when a lazy object registers with another lazy object.
  • When a lazy object registers with another lazy object, automatically use registerWithObservables instead. This would make it difficult to unregister, though, so we'd have to be on the lookout for corner cases.
  • We might add a new method that would make it possible for the lazy object A to tell B that it should start forwarding notifications again. This method would be called by lazy objects after calculations. However, it would be an additional loop (possibly a recursive one) performed after calculations.

All of these solutions have pros and cons and should be timed in stress scenarios. Suggestions are welcome.

@pcaspers
Copy link
Contributor Author

I'd be in favour of 2 -
1 seems to be error-prone since it requires manual work
if I remember correctly 3 was introduced as a workaround only (me not being aware of 1)
4 sounds complicated and we should only go for that if 2 turns out to cause a slowdown?

I am happy to change the PR to do 2 and run some tests on our side to check the performance.

@pcaspers
Copy link
Contributor Author

I made this change for "2" in the last commit here. I am not sure if it is the best way (in terms of performance) to make registerWith() virtual and overwrite it in LazyObject, but it seems to be the most straightforward way to do this? On the other hand

@pcaspers
Copy link
Contributor Author

ok so MSVC complains about this

warning C4250: 'QuantLib::CapFloorTermVolSurface': inherits 'QuantLib::LazyObject::QuantLib::LazyObject::registerWith' via dominance

and other similar cases, but before trying to fix this we should probably decide whether this is the way to go?

@ralfkonrad
Copy link
Contributor

Hi Peter,

can you try to build the whole PR on VS 2022 with the options /W4 and also with CMAKE_CXX_STANDARD 17?

I could replicate the memory leak I have with the change to the virtual inheritance in class Event : public virtual Observable in a small scenario here: https://github.com/ralfkonrad/CompilerBugWithVirtualInheritance

It doesn't have any warnings but crashes on VS 2022 with CMAKE_CXX_STANDARD 14

> compilerbug.exe
Running ChildWithNoProblem...
         Casting ChildWithNoProblem...
         Done...
Running ChildWithProblem...
         Casting ChildWithProblem...

Process finished with exit code -1073741819 (0xC0000005)

but does not compile with CMAKE_CXX_STANDARD 17 and /W4 /WX:

compilerbug.cpp(29): error C2220: the following warning is treated as an error
compilerbug.cpp(29): warning C4100: '$initVBases': unreferenced formal parameter

Without /WX (warning as errors) I get the warning C4100 but it works:

> compilerbug.exe
Running ChildWithNoProblem...
         Casting ChildWithNoProblem...
         Done...
Running ChildWithProblem...
         Casting ChildWithProblem...
         Done...
We never get here...

Process finished with exit code 0

Regards Ralf

@pcaspers
Copy link
Contributor Author

Hi @ralfkonrad so in short the problem goes away when enabling the c++17 standard (except for the compiler warning you get)?

@ralfkonrad
Copy link
Contributor

Yeah, looks like that.

Also the code doesn't cause problems on Ubuntu 22.04 using the default gcc and clang compilers.

@pcaspers
Copy link
Contributor Author

It could be worth sending a bug report to microsoft on that including your minimal example? I guess this warning

compilerbug.cpp(29): warning C4100: '$initVBases': unreferenced formal parameter

is also a bug.

@ralfkonrad
Copy link
Contributor

It could be worth sending a bug report to microsoft on that including your minimal example?

Yes, that was the idea behind this example.

Not sure about the warning C4100 though as the Abstract base class is indeed not fully initialized using virtual inheritance here. Perhaps the warning is not very precise but that's often the case in c++.

@sweemer: Do you have an opinion here?! The minimal example is "inspired" by a bug on our side using the virtual inheritance in class Event : public virtual Observable being introduced in this PR.

@ralfkonrad
Copy link
Contributor

ralfkonrad commented Jan 26, 2023

@pcaspers Besides that I have also tested the performance and the PR doesn't look to have an impact (on our use case). 👍

@sweemer
Copy link
Contributor

sweemer commented Jan 27, 2023

Hi all, I'm still trying to get caught up to speed on these changes. I can't yet say I fully understand it, but I do have some initial high-level thoughts.

  • Is there a chance that users will want to make other types of cashflows inherit from LazyObject in the future? If so, why not make CashFlow itself inherit from LazyObject? Or if not CashFlow, then what about Coupon?
  • As was mentioned, alwaysForwardNotifications seems to be error-prone and hard to enforce proper usage of. I know it's not going to be easy to change the existing design, but is there perhaps a way to design the dependencies between LazyObjects in such a way that both deep and shallow updates get automatically propagated in the right way? Similar to how Excel recalculates only the dirty cells.

Regarding the compiler bug, hopefully Microsoft can quickly fix it based upon your nice repro.

@ralfkonrad
Copy link
Contributor

Here is the corresponding bug report: https://developercommunity.visualstudio.com/t/Visual-Studio-2022-Segmentation-Fault-wi/10263539

@sweemer
Copy link
Contributor

sweemer commented Jan 30, 2023

@ralfkonrad By the way, does your real code where you first encountered the memory leak also delegate the explicit constructor to the copy or move constructor? If so, can you try implementing the explicit constructor without delegation and see if that fixes the issue? Of course that doesn't help with the compiler bug but it might help unblock you or others in the meantime.

@ralfkonrad
Copy link
Contributor

@sweemer Yes, the real code also delegate the explicit constructor to the copy or move constructor.

However as often, implementing the explicit constructor without delegation is not as straight forward as in the simplified example.

We have class OurFloatingRateCoupon : public ql::FloatingRateCoupon. Therefore we would end up with something like

class OurFloatingRateCoupon {
public:
  OurFloatingRateCoupon (const shared_ptr<Cashflow>& cf) 
    : ql::FloatingRateCoupon(
      makeCF(cf).a(),
      makeCF(cf).lot(),
      makeCF(cf).of(),
      makeCF(cf).parameters(),
      ...,
    ) {}

private:
  static FloatingRateCoupon makeCF(const shared_ptr<Cashflow>& cf) {
    // Do the casting etc.
    return FloatingRateCoupon(
      a,
      lot,
      of,
      parameters,
      ...
    );
  }
}

Also ugly...


The "workaround" ChildWithNoProblem works for us even if I don't understand why this is valid but the other isn't?! 🤔

class ChildWithNoProblem : public Parent {
  public:
    ChildWithNoProblem() = default;
    explicit ChildWithNoProblem(const std::shared_ptr<Parent>& parent)
    : ChildWithNoProblem(*makeChild(parent)) {}

  private:
    static std::shared_ptr<ChildWithNoProblem> makeChild(const std::shared_ptr<Parent>& parent) {
        std::cout << "\t Casting ChildWithNoProblem..." << std::endl;
        auto child = std::dynamic_pointer_cast<ChildWithNoProblem>(parent);
        std::cout << "\t Done..." << std::endl;
        return std::make_shared<ChildWithNoProblem>();
    }
};

@pcaspers
Copy link
Contributor Author

pcaspers commented Feb 8, 2023

@lballabio this comment might have gone unnoticed:

"ok so MSVC complains about this

warning C4250: 'QuantLib::CapFloorTermVolSurface': inherits 'QuantLib::LazyObject::QuantLib::LazyObject::registerWith' via dominance

and other similar cases, but before trying to fix this we should probably decide whether this is the way to go?"

I am happy to resolve this unless you say we should further explore the approach to take? Of the four alternatives you presented initially, this one seems the most straightforward + least error-prone one. If we should run into performance issues, we can still refine the approach?

@sweemer
Copy link
Contributor

sweemer commented Feb 9, 2023

I think that warning can be suppressed, because it's just informational and not actually highlighting anything unexpected.

Speaking of possibly unnoticed comments, any thoughts about my idea to move the LazyObject higher up in the hierarchy? I still think it's likely that people will want to apply this pattern to more generic cash flows in the future and we may as well plan for that now.

@lballabio
Copy link
Owner

Yes, let's go ahead. I think we can avoid the warning by giving CapFloorTermVolSurface an explicit registerWith that calls LazyObject::registerWith. Failing that, I guess we can suppress the warning.

@pcaspers
Copy link
Contributor Author

ok thanks, I'll try to get rid of the warnings

@sweemer not sure about the best level to introduce LazyObject in the inheritance tree (well, it's not a tree, unfortunately...). So far my intuition was to do this as far down (i.e. towards the most specialised classes) as possible and only if actually backed up by a real use case to avoid introducing unnecessary overhead for classes that actually do not benefit from the lazy evaluation. Although can lazy evaluation ever be bad .. :). So maybe it makes sense what you are suggesting.

@ralfkonrad
Copy link
Contributor

ralfkonrad commented Jul 11, 2023

Hmm, I'd be surprised if the lazy coupon was the problem. The old non-lazy coupon was always notifying. The lazy one either decreases the number of notifications (old behavior) or keeps it the same (new behavior). Unless the problem is in the caching code or the couple of dynamic casts in the new code.

True! I fully agree,

But isn't there a second aspect here!? When FloatingRateCoupon became lazy it also became an Observer. Before that it was only an Observable via Event. So perhaps now we get more notifications because the FloatingRateCoupon itself gets notified (by whom I have no concrete idea) and forwards this one which it didn't before?

@ralfkonrad
Copy link
Contributor

No, sorry. It was an Observer...

@lballabio
Copy link
Owner

One thing that might mitigate the problems with only forwarding the first notification: the tricky cases are those in which the evaluation date changes, right? And in those cases, we should probably recalculate anyway. What if the constructor of Instrument registered with the evaluation date, no matter what? This way they would at least receive that notification, even if their underlying lazy objects don't notify them.

@ralfkonrad
Copy link
Contributor

Two big differences I see are that the former

Rate FloatingRateCoupon::rate() const {
    QL_REQUIRE(pricer_, "pricer not set");
    pricer_->initialize(*this);
    return pricer_->swapletRate();
}

void FloatingRateCoupon::update() override { notifyObservers(); }

now effectively became

Rate FloatingRateCoupon::rate() const {
    if (!calculated_ && !frozen_) {
        calculated_ = true;   // prevent infinite recursion in
                              // case of bootstrapping
        try {
            QL_REQUIRE(pricer_, "pricer not set");
            pricer_->initialize(*this);
            rate_ = pricer_->swapletRate();
        } catch (...) {
            calculated_ = false;
            throw;
        }
    }
    return rate_;
}

void LazyObject::update() {
    if (updating_) {
        #ifdef QL_THROW_IN_CYCLES
        QL_FAIL("recursive notification loop detected; you probably created an object cycle");
        #else
        return;
        #endif
    }

    // This sets updating to true (so the above check breaks the
    // infinite loop if we enter this method recursively) and will
    // set it back to false when we exit this scope, either
    // successfully or because of an exception.
    UpdateChecker checker(this);

    // forwards notifications only the first time
    if (calculated_ || alwaysForward_) {
        // set to false early
        // 1) to prevent infinite recursion
        // 2) otherways non-lazy observers would be served obsolete
        //    data because of calculated_ being still true
        calculated_ = false;
        // observers don't expect notifications from frozen objects
        if (!frozen_)
            notifyObservers();
            // exiting notifyObservers() calculated_ could be
            // already true because of non-lazy observers
    }
}

including

  • couple of extra if (...) {...} blocks
  • costly(?) UpdateChecker checker(this); creation and destruction
  • costly(?) try {...} catch (...) {...} block
  • writing access to mutable rate_

But can this explain the performance impact @djkrystul sees?

@lballabio
Copy link
Owner

lballabio commented Jul 11, 2023

I think it can't — because with the very same code and alwaysForward_ = False the performance was back to normal.

@lballabio
Copy link
Owner

One thing that might mitigate the problems with only forwarding the first notification: the tricky cases are those in which the evaluation date changes, right? And in those cases, we should probably recalculate anyway. What if the constructor of Instrument registered with the evaluation date, no matter what? This way they would at least receive that notification, even if their underlying lazy objects don't notify them.

@pcaspers — thoughts?

@ralfkonrad
Copy link
Contributor

I think it can't — because with the very same code and alwaysForward_ = False the performance was back to normal.

Good point.

On the other hand: I saw an impact of the change by ~25% already (16sec to 20sec, #1566 (comment)) and I think I haven't tested against the latest ones like the UpdateChecker checker(this);.

@djkrystul
Copy link

djkrystul commented Jul 11, 2023

Does this approach somehow fit into your sensi calculation too? Can you check if this also reduces your calculation times to the prior ones?

I see that you compute parallel shift sensi. In your case your trick works nicely.
We compute bucketed par deltas. So each quote is bumped independently, then the whole portfolio is revalued and then we set the quote back to original value. So for each bump and reset back there is a full recalibration of all curves involved and I guess a lot of notifications are happening during calibration. And I think this makes it slow as a lot's of curves are linked via basis to each other, so all move at the same time when there is a change to a back-bone quote.
Disabling notifications for all swaps before bumps and resets may help, so we could avoid all notifications from recalibrations. Wondering how fast this will be. I will try to make a test example later, a bit busy now.

@djkrystul
Copy link

But now I am thinking that this will not work, i.e. if I disable notifications in settings, then bumps to quotes will not have any effect. So this approach works for you as you probably shift curves in zero space or so and not via quotes which require notifications in order to trigger recalibration, right?

@ralfkonrad
Copy link
Contributor

No, probably you would have to trigger recalibration manually like I "somhow" do with the swap->deepUpdate();. Without swap->deepUpdate(); my code also wouldn't work.

What you can also try is to enable QL_THROW_IN_CYCLES. Perhaps you have loops of notification in your code which can be optimized.

inline void LazyObject::update() {
if (updating_) {
#ifdef QL_THROW_IN_CYCLES
QL_FAIL("recursive notification loop detected; you probably created an object cycle");
#else
return;
#endif
}

@ralfkonrad
Copy link
Contributor

But for me that sounds like you probably do not have a lazy FloatingRateCoupon issue but an issue with notifications during recalibration?!

@djkrystul
Copy link

But for me that sounds like you probably do not have a lazy FloatingRateCoupon issue but an issue with notifications during recalibration?!

Indeed, looks like iterative recalibration is a problem, as I said earlier, all curves recalibrate at the time, and disabling notifications is not possible in this case. I just need to check if this is indeed the case and think of some optimization if possible at all.

@ralfkonrad
Copy link
Contributor

So it might be the case that you even recalibrate too often, e.g. some intermediate notifications trigger a recalibration and the next forwarded one also triggers one?!

@djkrystul
Copy link

For example, assume discounting curve G(q+s) depends on quotes q + s. And assume index (forward) curve F is a function of discounting curve G and quotes q: F( G(q+s), q).
If q changes, then curve F starts recalibration, at the same time discounting curve G will also start recalibration. This will happen iteratively untill both will converge. I think in this process we get a lot of notifications. Same happens when we change spread s.

@lballabio
Copy link
Owner

One thing that might mitigate the problems with only forwarding the first notification: the tricky cases are those in which the evaluation date changes, right? And in those cases, we should probably recalculate anyway. What if the constructor of Instrument registered with the evaluation date, no matter what? This way they would at least receive that notification, even if their underlying lazy objects don't notify them.

Ok, now I'm really confused. If I try doing that, I'm getting errors from notification cycles. Which... doesn't make sense to me at all. Any ideas as to why that might be?

@pcaspers
Copy link
Contributor Author

One thing that might mitigate the problems with only forwarding the first notification: the tricky cases are those in which the evaluation date changes, right? And in those cases, we should probably recalculate anyway. What if the constructor of Instrument registered with the evaluation date, no matter what? This way they would at least receive that notification, even if their underlying lazy objects don't notify them.

@pcaspers — thoughts?

@lballabio you mean we could revert to forwarding the first notification only and instead register instrument with the evaluation date? And that would be equivalent to forwarding all notifications?

@lballabio
Copy link
Owner

@pcaspers that was the idea — I thought that could work, but see my later comment...

@pcaspers
Copy link
Contributor Author

I am not sure if that solves all possible instances. This is what I wrote above:

Can we take a step back for a moment. I stumbled across the following situation in our code (arrow = notifies)

A -> B -> C

where A is a lazy object, B is not, C is a lazy object. Assume that the calculation of C never triggers the calculation of A or B. Now if A only forwards the first notification, C won't be notified if B and C are not registered themselves with the relevant observable.

This is independent of the change we are discussing here, but I think it shows that

the default behaviour of LazyObject not to forward all notifications can lead to a wrong behaviour
and solution 2 we are discussing here does not ensure all required notifications are sent in general
I wonder if we might want to always forward notifications from lazy objects by default and leave it to certain situations to turn them off (and thereby optimising the code).

I don't think the global evaluation date was part of the chain A -> B -> C here. I don't remember the actual classes I was referring to here, but doesn't that show already on an abstract level that we need to always forward notifications to ensure correct behaviour?

@lballabio
Copy link
Owner

The idea was that the reason A and B are not calculated is almost always (yes, almost) that they're expired. When the evaluation date doesn't change, they stay expired. If the evaluation date changes, they might be alive but in 1.30 don't send a notification. If C registers with the evaluation date, A and B still won't notify, but C will receive a notification from the evaluation date and will recalculate anyway,

It doesn't solve all cases, but the ones we bumped into were probably of this kind.

However, it turns out that registering all instruments with the evaluation date causes notification loops to be reported. I have no idea why.

@pcaspers
Copy link
Contributor Author

Not sure. Think about something like Swaption -> Swap -> Coupon and a swaption engine that does not calculate the swap. The issue has nothing to do with expired instruments and evaluation date changes then. This does not match my abstract example above, since B = Swap is lazy, but it's easy to imagine a similar situation where the intermediate object is no lazy I guess. In fact I am pretty sure I saw an example for this in ORE.

Anyway, shouldn't LazyObject work reliably in 100% of the cases by default?

@pcaspers
Copy link
Contributor Author

However, it turns out that registering all instruments with the evaluation date causes notification loops to be reported. I have no idea why.

It's the library's way of saying "enough, enough, go for the straightforward solution!" :-)

@ralfkonrad
Copy link
Contributor

Regarding the loop errors: For me they start with the Markov functional model tests and then they are continuing.

If I run only the Markov functional model tests standalone in Clion, they succeed. Looks to me like an isolated problem.

So I removed tests from the suite and ended up with one test causing the error: LazyObjectTest::testNotificationLoop

Running the test suite without it works fine. But I have no clue why, especially why the loop is detected so much later in the suite?!

test_suite* LazyObjectTest::suite() {
    auto* suite = BOOST_TEST_SUITE("LazyObject tests");
    suite->add(QUANTLIB_TEST_CASE(&LazyObjectTest::testDiscardingNotifications));
    suite->add(QUANTLIB_TEST_CASE(&LazyObjectTest::testForwardingNotificationsByDefault));
//    suite->add(QUANTLIB_TEST_CASE(&LazyObjectTest::testNotificationLoop));
    return suite;
}

image

image

@sweemer
Copy link
Contributor

sweemer commented Jul 11, 2023

What if the constructor of Instrument registered with the evaluation date, no matter what?

A lot of Instrument subclasses do this already, and I think it makes sense to standardize this relationship at the Instrument level.

But does this mean that Instrument::valuationDate should always be in sync with Settings::evaluationDate? Or perhaps this member can be deprecated and kept in Instrument::results only?

These questions probably deserve a separate issue, unless there's a quick and easy explanation.

@lballabio
Copy link
Owner

Unfortunately, fossile records show that for some reason the discounting bond and swap engines set valuationDate to the date at which we're discounting—it could be the evaluation date, it could be the corresponding spot date. I agree it's probably a misnomer. Do you want to open an issue so we have a place to think about it?

@djkrystul
Copy link

djkrystul commented Jul 12, 2023

I have run profiler on a simple test with dual curve bootstrap where I compute par deltas of ~1000 swaps. See the screenshot below. On the left is the result with alwaysForward_ = true, on the left with alwaysForward_ = false.

profiler

I will clean the code a bit and share it with you. Basically in this special example both curves recalibrate at the same time on each quote bump until they both converge, so there is no infinite loop. During this they fire up lot's of notifications.

The slow down on this specific example was ~factor 10, in real application can be even worse because of many curves involved.

@lballabio
Copy link
Owner

Ok — when you have an example, please open a new issue. Thanks!

@djkrystul
Copy link

djkrystul commented Jul 12, 2023

Here is the code to reproduce the slow down I was talking about:

#include <chrono>
#include <iostream>
#include <ql/time/calendars/target.hpp>
#include <ql/quotes/compositequote.hpp>
#include <ql/indexes/ibor/estr.hpp>
#include <ql/time/daycounters/actual360.hpp>
#include <ql/termstructures/yield/oisratehelper.hpp>
#include <ql/termstructures/yield/piecewiseyieldcurve.hpp>
#include <ql/instruments/makeois.hpp>
#include <ql/indexes/ibor/euribor.hpp>
#include <ql/time/daycounters/thirty360.hpp>
#include <ql/instruments/makevanillaswap.hpp>

using namespace QuantLib;

namespace performance_test {

	void stopTimer(const std::chrono::steady_clock::time_point& start) {
		auto stop = std::chrono::steady_clock::now();
		double seconds = std::chrono::duration_cast<std::chrono::milliseconds>(stop - start).count() * 1e-3;
		int hours = int(seconds / 3600);
		seconds -= hours * 3600;
		int minutes = int(seconds / 60);
		seconds -= minutes * 60;
		std::cout << "\nRun completed in ";
		if (hours > 0)
			std::cout << hours << " h ";
		if (hours > 0 || minutes > 0)
			std::cout << minutes << " m ";
		std::cout << std::fixed << std::setprecision(0)
			<< seconds << " s\n" << std::endl;
	}

	struct Datum {
		Integer settlementDays;
		Integer n;
		TimeUnit unit;
		Rate rate;
		Spread spread;
	};

	QuantLib::Real minus(QuantLib::Real x, QuantLib::Real y) {
		return x - y;
	}
}


using namespace QuantLib;


int main(int, char* []) {    
    performance_test::Datum depositData = { 0, 1, Days, 1.10, 0.1 };
    performance_test::Datum rateHelpersData[] = {
        { 2,  1, Weeks, 1.245, 0.1},
        { 2,  2, Weeks, 1.269, 0.1},
        { 2,  3, Weeks, 1.277, 0.1},
        { 2,  1, Months, 1.281, 0.1},
        { 2,  2, Months, 1.18, 0.1},
        { 2,  3, Months, 1.143, 0.1},
        { 2,  4, Months, 1.125, 0.1},
        { 2,  5, Months, 1.116, 0.1},
        { 2,  6, Months, 1.111, 0.1},
        { 2,  7, Months, 1.109, 0.1},
        { 2,  8, Months, 1.111, 0.1},
        { 2,  9, Months, 1.117, 0.1},
        { 2, 10, Months, 1.129, 0.1},
        { 2, 11, Months, 1.141, 0.1},
        { 2, 12, Months, 1.153, 0.1},
        { 2, 15, Months, 1.218, 0.1},
        { 2, 18, Months, 1.308, 0.1},
        { 2, 21, Months, 1.407, 0.1},
        { 2,  2,  Years, 1.510, 0.1},
        { 2,  3,  Years, 1.916, 0.1},
        { 2,  4,  Years, 2.254, 0.1},
        { 2,  5,  Years, 2.523, 0.1},
        { 2,  6,  Years, 2.746, 0.1},
        { 2,  7,  Years, 2.934, 0.1},
        { 2,  8,  Years, 3.092, 0.1},
        { 2,  9,  Years, 3.231, 0.1},
        { 2, 10,  Years, 3.380, 0.1},
        { 2, 11,  Years, 3.457, 0.1},
        { 2, 12,  Years, 3.544, 0.1},
        { 2, 15,  Years, 3.702, 0.1},
        { 2, 20,  Years, 3.703, 0.1},
        { 2, 25,  Years, 3.541, 0.1},
        { 2, 30,  Years, 3.369, 0.1},
        { 2, 40,  Years, 3.369, 0.1},
        { 2, 50,  Years, 3.369, 0.1}
    };

    try {
        auto start = std::chrono::steady_clock::now();

        // Set evaluation date
        Date evaluationDate = Settings::instance().evaluationDate();

        RelinkableHandle<YieldTermStructure> discountingHandle, estrHandle, euriborHandle;

        auto estr = ext::make_shared<Estr>(estrHandle);
        auto euribor6M = ext::make_shared<Euribor>(6 * Months, euriborHandle);
        const Natural settlementDays = 2;
        const Natural paymentLag = 1;
        const Real nominal = 100000.0;

        Calendar calendar = estr->fixingCalendar();
        bool telescopicValueDates = true;

        std::vector<ext::shared_ptr<RateHelper>> oisRateHelpers, euriborRateHelpers;
        std::vector<ext::shared_ptr<SimpleQuote>> euriborQuotes, spreadQuotes;
        std::vector<ext::shared_ptr<Quote>> oisQuotes;

        Real rate = 0.01 * depositData.rate;
        Real spread = 0.01 * depositData.spread;
        Period term = depositData.n * depositData.unit;
        oisQuotes.push_back(ext::make_shared<SimpleQuote>(rate - spread));
        euriborQuotes.push_back(ext::make_shared<SimpleQuote>(rate));

        oisRateHelpers.push_back(ext::make_shared<DepositRateHelper>(Handle<Quote>(oisQuotes.back()), estr));

        typedef QuantLib::Real(*binary_f)(QuantLib::Real, QuantLib::Real);

        for (auto& i : rateHelpersData) {
            rate = 0.01 * i.rate;
            spread = 0.01 * i.spread;

            euriborQuotes.push_back(ext::make_shared<SimpleQuote>(rate));
            spreadQuotes.push_back(ext::make_shared<SimpleQuote>(spread));

            oisQuotes.push_back(ext::make_shared<CompositeQuote<binary_f>>(Handle<Quote>(euriborQuotes.back()), Handle<Quote>(spreadQuotes.back()), performance_test::minus));
            term = i.n * i.unit;

            ext::shared_ptr<RateHelper> oisHelper = ext::make_shared<OISRateHelper>(
                i.settlementDays,
                term,
                Handle<Quote>(oisQuotes.back()),
                estr,
                Handle<YieldTermStructure>(),
                telescopicValueDates,
                paymentLag,
                Following,
                Annual,
                estr->fixingCalendar(),
                0 * Days,
                0.0,
                Pillar::LastRelevantDate,
                Date());

            ext::shared_ptr<RateHelper> euriborHelper = ext::make_shared<SwapRateHelper>(
                Handle<Quote>(euriborQuotes.back()),
                term,
                euribor6M->fixingCalendar(),
                Annual,
                ModifiedFollowing,
                Thirty360(Thirty360::BondBasis),
                euribor6M,
                Handle<Quote>(), //spread
                0 * Days,
                discountingHandle,
                2L,
                Pillar::LastRelevantDate,
                Date());

            oisRateHelpers.push_back(oisHelper);
            euriborRateHelpers.push_back(euriborHelper);
        }

        auto estrTS = ext::make_shared<PiecewiseYieldCurve<ZeroYield, Linear>>(evaluationDate, oisRateHelpers, Actual365Fixed());        
        estrTS->enableExtrapolation();
        estrHandle.linkTo(estrTS);
        discountingHandle.linkTo(estrTS);

        auto euriborTS = ext::make_shared<PiecewiseYieldCurve<ZeroYield, Linear>>(evaluationDate, euriborRateHelpers, Actual365Fixed());        
        euriborTS->enableExtrapolation();
        euriborHandle.linkTo(euriborTS);
                
        std::cout << "\nEstr curve: " << std::endl;

        std::vector<Date> datesEstr = estrTS->dates();
        for (const auto& date : datesEstr) {
            std::cout << date << "\t\t\t" << estrHandle->zeroRate(date, Actual360(), Continuous).rate() << std::endl;
        }

        std::cout << "\nEuribor curve: " << std::endl;
        for (const auto& date : datesEstr) {
            std::cout << date << "\t\t\t" << euriborHandle->zeroRate(date, Actual360(), Continuous).rate() << std::endl;
        }

        // build swaps 
        Size oisN = 100;
        Size euriborN = 1000;
        std::vector<ext::shared_ptr<Instrument>> swaps;
        swaps.reserve(oisN + euriborN);

        Period swapTenor = 35 * 365 * Days;
        Period tenor;
        Real fixedRate = 0.02;
        Date settlement = calendar.advance(evaluationDate, settlementDays * Days, Following);

        performance_test::stopTimer(start);

        std::cout << "\nBuilding swaps: " << std::endl;

        for (Size i = 0; i < oisN; ++i) {
            tenor = swapTenor + 2 * Days;
            ext::shared_ptr<Instrument> swap = MakeOIS(swapTenor, estr, fixedRate, 0 * Days)
                .withEffectiveDate(settlement)
                .withNominal(nominal)
                .withPaymentLag(paymentLag)
                .withDiscountingTermStructure(discountingHandle)
                .withTelescopicValueDates(false).operator boost::shared_ptr<QuantLib::OvernightIndexedSwap>();

            swaps.push_back(std::move(swap));
        }

        for (Size i = 0; i < euriborN; ++i) {
            tenor = swapTenor + 2 * Days;
            ext::shared_ptr<Instrument> swap = MakeVanillaSwap(tenor, euribor6M, fixedRate).withDiscountingTermStructure(discountingHandle).operator boost::shared_ptr<QuantLib::VanillaSwap>();
            swaps.push_back(std::move(swap));
        }
        performance_test::stopTimer(start);

        std::cout << "\nComputing ref NPV: " << std::endl;

        Real refNPV = 0.0;
        for (const auto& swap : swaps) {
            refNPV += swap->NPV();
        }
        std::cout << "\nReference NPV: " << refNPV << std::endl;
        performance_test::stopTimer(start);

        std::vector<Real> parDeltas;
        parDeltas.reserve(spreadQuotes.size() + euriborQuotes.size());

        std::cout << "\nComputing Spread deltas: " << std::endl;

        // bump quotes
        Real originalValue;
        for (const auto& q : spreadQuotes) {
            originalValue = q->value();
            q->setValue(originalValue + 0.0001);
            Real npv = 0.0;
            for (const auto& swap : swaps) {
                npv += swap->NPV();
            }
            parDeltas.push_back(npv - refNPV);
            q->setValue(originalValue);
        }
        performance_test::stopTimer(start);

        std::cout << "\nComputing Outright deltas: " << std::endl;
        for (const auto& q : euriborQuotes) {
            originalValue = q->value();
            q->setValue(originalValue + 0.0001);
            Real npv = 0.0;
            for (const auto& swap : swaps) {
                npv += swap->NPV();
            }
            parDeltas.push_back(npv - refNPV);
            q->setValue(originalValue);
        }

        for (const auto& d : parDeltas) {
            std::cout << "Par Delta: " << d << std::endl;
        }
        performance_test::stopTimer(start);
        return 0;
    }
    catch (const std::exception& e) {
        std::cout << "Testing par deltas of swaps with dual curves failed. Error: " << e.what();
        return 1;
    }
}

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.

6 participants