Skip to content
All library documents

Combining Yield Curves by Multiplying Discount Factors

Article Quant Q&A · Author: Lisa Ann

Summary

The document explains how to combine a base yield curve with a term structure of credit spreads in QuantLib. It notes that the library’s constant-spread curve is a useful model, but a curve built from two separate term structures requires a custom class derived from YieldTermStructure that holds handles to both curves.

The key relationship is that adding continuously compounded rates corresponds to multiplying their discount factors. The proposed implementation therefore overrides the discount-factor method and returns the product of the two curves’ discount factors at a given time. This lets the curve’s interpolation handle times between observed maturities, including maturities absent from the spread inputs.

The example uses deposit and swap instruments to build a base curve and swap helpers to represent credit spreads. It does not provide a complete implementation or address calibration choices, credit-risk modeling assumptions, or validation of the resulting curve; those details must be handled separately.

Key ideas

  • A custom yield term structure can combine two input curves by holding handles to each one.
  • Adding rates corresponds to multiplying their discount factors.
  • Override the discount-factor calculation to return the product of the base and spread curve discount factors.
  • The input curves’ interpolation determines values at times between their observed maturities.

Tags

Full text
# How to sum interest rate curves in QuantLib


# How to sum interest rate curves in QuantLib












C++ code taken from Bonds.cpp and slightly amended:

```
#include <ql/quantlib.hpp>
#include <boost/timer.hpp>
#include <iostream>
#include <iomanip>

using namespace QuantLib;

int main(int, char* []) {

    try {

        // Just a couple of parameters

        Calendar calendar = TARGET();
        Date settlementDate(18, September, 2008);
        settlementDate = calendar.adjust(settlementDate);
        Integer fixingDays = 3;
        Natural settlementDays = 3;
        Date todaysDate = calendar.advance(settlementDate, -fixingDays, Days);
        Settings::instance().evaluationDate() = todaysDate;

        /* Now let's introduce two curves: the first one is a swap curve built from 
        deposits and IRS, the second one is a credit spread curve which should be 
        added to the former one to get a proper yield curve */

        // Swap curve

        DayCounter termStructureDayCounter = ActualActual(ActualActual::ISDA);
        double tolerance = 1.0e-5;

        // Deposits

        Rate d1wQuote = 0.043375;
        Rate d1mQuote = 0.031875;
        Rate d3mQuote = 0.0320375;
        Rate d6mQuote = 0.03385;
        Rate d9mQuote = 0.0338125;
        Rate d1yQuote = 0.0335125;

        boost::shared_ptr<Quote> d1wRate(new SimpleQuote(d1wQuote));
        boost::shared_ptr<Quote> d1mRate(new SimpleQuote(d1mQuote));
        boost::shared_ptr<Quote> d3mRate(new SimpleQuote(d3mQuote));
        boost::shared_ptr<Quote> d6mRate(new SimpleQuote(d6mQuote));
        boost::shared_ptr<Quote> d9mRate(new SimpleQuote(d9mQuote));
        boost::shared_ptr<Quote> d1yRate(new SimpleQuote(d1yQuote));

        // IRS

        Rate s2yQuote = 0.0295;
        Rate s3yQuote = 0.0323;
        Rate s5yQuote = 0.0359;
        Rate s10yQuote = 0.0412;
        Rate s15yQuote = 0.0433;

        boost::shared_ptr<Quote> s2yRate(new SimpleQuote(s2yQuote));
        boost::shared_ptr<Quote> s3yRate(new SimpleQuote(s3yQuote));
        boost::shared_ptr<Quote> s5yRate(new SimpleQuote(s5yQuote));
        boost::shared_ptr<Quote> s10yRate(new SimpleQuote(s10yQuote));
        boost::shared_ptr<Quote> s15yRate(new SimpleQuote(s15yQuote));

        // Rate Helper

        // Deposits
        DayCounter depositDayCounter = Actual360();

        boost::shared_ptr<RateHelper> d1w(new DepositRateHelper(
                Handle<Quote>(d1wRate),
                1*Weeks, fixingDays,
                calendar, ModifiedFollowing,
                true, depositDayCounter));
        boost::shared_ptr<RateHelper> d1m(new DepositRateHelper(
                Handle<Quote>(d1mRate),
                1*Months, fixingDays,
                calendar, ModifiedFollowing,
                true, depositDayCounter));
        boost::shared_ptr<RateHelper> d3m(new DepositRateHelper(
                Handle<Quote>(d3mRate),
                3*Months, fixingDays,
                calendar, ModifiedFollowing,
                true, depositDayCounter));
        boost::shared_ptr<RateHelper> d6m(new DepositRateHelper(
                Handle<Quote>(d6mRate),
                6*Months, fixingDays,
                calendar, ModifiedFollowing,
                true, depositDayCounter));
        boost::shared_ptr<RateHelper> d9m(new DepositRateHelper(
                Handle<Quote>(d9mRate),
                9*Months, fixingDays,
                calendar, ModifiedFollowing,
                true, depositDayCounter));
        boost::shared_ptr<RateHelper> d1y(new DepositRateHelper(
                Handle<Quote>(d1yRate),
                1*Years, fixingDays,
                calendar, ModifiedFollowing,
                true, depositDayCounter));

        // Setup IRS
        Frequency swFixedLegFrequency = Annual;
        BusinessDayConvention swFixedLegConvention = Unadjusted;
        DayCounter swFixedLegDayCounter = Thirty360(Thirty360::European);
        boost::shared_ptr<IborIndex> swFloatingLegIndex(new Euribor6M);

        const Period forwardStart(1*Days);

        boost::shared_ptr<RateHelper> s2y(new SwapRateHelper(
                Handle<Quote>(s2yRate), 2*Years,
                calendar, swFixedLegFrequency,
                swFixedLegConvention, swFixedLegDayCounter,
                swFloatingLegIndex, Handle<Quote>(),forwardStart));
        boost::shared_ptr<RateHelper> s3y(new SwapRateHelper(
                Handle<Quote>(s3yRate), 3*Years,
                calendar, swFixedLegFrequency,
                swFixedLegConvention, swFixedLegDayCounter,
                swFloatingLegIndex, Handle<Quote>(),forwardStart));
        boost::shared_ptr<RateHelper> s5y(new SwapRateHelper(
                Handle<Quote>(s5yRate), 5*Years,
                calendar, swFixedLegFrequency,
                swFixedLegConvention, swFixedLegDayCounter,
                swFloatingLegIndex, Handle<Quote>(),forwardStart));
        boost::shared_ptr<RateHelper> s10y(new SwapRateHelper(
                Handle<Quote>(s10yRate), 10*Years,
                calendar, swFixedLegFrequency,
                swFixedLegConvention, swFixedLegDayCounter,
                swFloatingLegIndex, Handle<Quote>(),forwardStart));
        boost::shared_ptr<RateHelper> s15y(new SwapRateHelper(
                Handle<Quote>(s15yRate), 15*Years,
                calendar, swFixedLegFrequency,
                swFixedLegConvention, swFixedLegDayCounter,
                swFloatingLegIndex, Handle<Quote>(),forwardStart));

        // A depo-swap curve
        std::vector<boost::shared_ptr<RateHelper> > depoSwapInstruments;
        depoSwapInstruments.push_back(d1w);
        depoSwapInstruments.push_back(d1m);
        depoSwapInstruments.push_back(d3m);
        depoSwapInstruments.push_back(d6m);
        depoSwapInstruments.push_back(d9m);
        depoSwapInstruments.push_back(d1y);
        depoSwapInstruments.push_back(s2y);
        depoSwapInstruments.push_back(s3y);
        depoSwapInstruments.push_back(s5y);
        depoSwapInstruments.push_back(s10y);
        depoSwapInstruments.push_back(s15y);
        boost::shared_ptr<YieldTermStructure> depoSwapTermStructure(
                new PiecewiseYieldCurve<Discount,LogLinear>(
                        settlementDate, depoSwapInstruments,
                        termStructureDayCounter,
                        tolerance));

        return 0;

    } catch (std::exception& e) {
        std::cerr << e.what() << std::endl;
        return 1;
    } catch (...) {
        std::cerr << "unknown error" << std::endl;
        return 1;
    }
}
```

If I'm not wrong, so far we've been obtaining a kind of risk free discount curve.

Now let me to introduce an additional credit spread curve in the form of an object handled by `RateHelper` (kinda snippet to be inserted after the depo-swap curve of the code above):

```
        ...
        Rate d1ySpread = 0.013;
        Rate d2ySpread = 0.011;
        Rate d3ySpread = 0.022;
        Rate d6ySpread = 0.0238;
        Rate d9ySpread = 0.0238;
        Rate d10ySpread = 0.0239;

        boost::shared_ptr<Quote> d1ySpreadRate(new SimpleQuote(d1ySpread));
        boost::shared_ptr<Quote> d2ySpreadRate(new SimpleQuote(d2ySpread));
        boost::shared_ptr<Quote> d3ySpreadRate(new SimpleQuote(d3ySpread));
        boost::shared_ptr<Quote> d6ySpreadRate(new SimpleQuote(d6ySpread));
        boost::shared_ptr<Quote> d9ySpreadRate(new SimpleQuote(d9ySpread));
        boost::shared_ptr<Quote> d10ySpreadRate(new SimpleQuote(d10ySpread));

        boost::shared_ptr<RateHelper> credit1y(new SwapRateHelper(
                Handle<Quote>(d1ySpreadRate), 1*Years,
                calendar, swFixedLegFrequency,
                swFixedLegConvention, swFixedLegDayCounter,
                swFloatingLegIndex, Handle<Quote>(),forwardStart));
        boost::shared_ptr<RateHelper> credit2y(new SwapRateHelper(
                Handle<Quote>(d2ySpreadRate), 2*Years,
                calendar, swFixedLegFrequency,
                swFixedLegConvention, swFixedLegDayCounter,
                swFloatingLegIndex, Handle<Quote>(),forwardStart));
        boost::shared_ptr<RateHelper> credit3y(new SwapRateHelper(
                Handle<Quote>(d3ySpreadRate), 3*Years,
                calendar, swFixedLegFrequency,
                swFixedLegConvention, swFixedLegDayCounter,
                swFloatingLegIndex, Handle<Quote>(),forwardStart));
        boost::shared_ptr<RateHelper> credit6y(new SwapRateHelper(
                Handle<Quote>(d6ySpreadRate), 6*Years,
                calendar, swFixedLegFrequency,
                swFixedLegConvention, swFixedLegDayCounter,
                swFloatingLegIndex, Handle<Quote>(),forwardStart));
        boost::shared_ptr<RateHelper> credit9y(new SwapRateHelper(
                Handle<Quote>(d9ySpreadRate), 9*Years,
                calendar, swFixedLegFrequency,
                swFixedLegConvention, swFixedLegDayCounter,
                swFloatingLegIndex, Handle<Quote>(),forwardStart));
        boost::shared_ptr<RateHelper> credit10y(new SwapRateHelper(
                Handle<Quote>(d10ySpreadRate), 10*Years,
                calendar, swFixedLegFrequency,
                swFixedLegConvention, swFixedLegDayCounter,
                swFloatingLegIndex, Handle<Quote>(),forwardStart));

        std::vector<boost::shared_ptr<RateHelper> > creditInstruments;
        creditInstruments.push_back(credit1y);
        creditInstruments.push_back(credit2y);
        creditInstruments.push_back(credit3y);
        creditInstruments.push_back(credit6y);
        creditInstruments.push_back(credit9y);
        creditInstruments.push_back(credit10y);
        boost::shared_ptr<YieldTermStructure> creditTermStructure(
                new PiecewiseYieldCurve<Discount,LogLinear>(
                        settlementDate, creditInstruments,
                        termStructureDayCounter,
                        tolerance));
        ...
```

The object `creditTermStructure` could represent, as instance, a CDS spread curve, or such a term structure.

Having both the `depoSwapTermStructure` and the `creditTermStructure` built by the code above, I would like to produce a new object of class `YieldTermStructure` by summing `depoSwapTermStructure` and `creditTermStructure` taking into account the misaligned time knots, that is, the code must sum the rates by the appropriate tenors (1Y depo-swap + 1Y credit spread, 2Y depo-swap + 2Y credit spread... and so on. As of the lacking credit spread maturities, such as 18Y, it should be interpolate known knots).

How is it possible to do such a thing using QuantLib?

## Answer by Luigi Ballabio (score 6, accepted)

https://quant.stackexchange.com/a/9404

There's no class at this time to add two curves as you want, but it won't be much difficult to write it.

The closest you'll get in the library is the `ZeroSpreadedTermStructure` class, that shows the general idea: it inherits from `YieldTermStructure` (by way of `ZeroYieldStructure`) takes a `YieldTermStructure` and a spread (constant, in this case) and override its own methods so that they return the sum of the two: for instance,

```
Rate ZeroSpreadedTermStructure::forwardImpl(Time t) const {
    return originalCurve_->forwardRate(t, t, comp_, freq_, true)
        + spread_->value();
}
```

In your case, you'll have to write a similar class that takes two `Handle`s to `YieldTermStructure` instead. Somewhat surprisingly, this will make your job easier. You can still take `ZeroSpreadedTermStructure` as a model for other tasks such as registering with the needed observers, but all you have to do in this case is inherit from `YieldTermStructure` directly and override the single `discountImpl` method:

```
DiscountFactor YourClass::discountImpl(Time t) const {
    return baseCurve_->discount(t, true)
         * spreadCurve_->discount(t, true);
}
```

since the sum of the rates implies the product of the corresponding discount factors.

If you need more information, a description of the `YieldTermStructure` hierarchy and the methods it implements (which is kind of longish to include here) is available at http://implementingquantlib.blogspot.com/2013/09/chapter-3-part-2-of-n-yield-term.html and later posts.

And of course, once you have your curve working, you're welcome to contribute it to QuantLib. The easier way would be to get a GitHub account and follow the instructions in the readme at https://github.com/lballabio/quantlib.

Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.