Building a Yield Curve in C++: Theory and Implementation

by Clement Daubrenet
yield curve

In fixed income markets, the yield curve is a fundamental tool that maps interest rates to different maturities. It underpins the pricing of bonds, swaps, and other financial instruments. How to build a yield curve in C++?

Traders, quants, and risk managers rely on it daily to discount future cash flows. This article explores how to build a basic zero-coupon yield curve using C++, starting from market data and ending with interpolated discount factors. We’ll also compare our results with QuantLib, the industry-standard quantitative finance library.

1. What’s a Yield Curve?

At its core, the yield curve captures how interest rates change with the length of time you lend money. Here’s an example of hypothetical yields across maturities:

MaturityYield (%)
Overnight (ON)1.00
1 Month1.20
3 Months1.35
6 Months1.50
1 Year1.80
2 Years2.10
5 Years2.60
10 Years3.00
30 Years3.20

Plotted on a graph, these points form the yield curve — typically upward sloping, as longer maturities usually command higher yields due to risk and time value of money.

Regular Yield Curve

However, in stressed environments, the curve can flatten or even invert (e.g., 2-year yield > 10-year yield), often signaling economic uncertainty:

Inverted Yield Curve

Here is the plot of a hypothetical inverted yield curve, where shorter-term yields are higher than longer-term ones. This often reflects market expectations of economic slowdown or interest rate cuts in the future.

When investors expect future economic weakness, they begin to anticipate interest rate cuts by the central bank. As a result:

  • Demand increases for long-term bonds (seen as safe havens), which pushes their prices up and their yields down.
  • Meanwhile, short-term rates may remain high due to current central bank policy (e.g. inflation control).

This flips the curve: short-term yields become higher than long-term yields.

2. Key Concepts Behind Yield Curve Construction

Before jumping into code, it’s important to understand how yield curves are built from market data. In practice, we don’t observe a complete yield curve directly, we build (or “bootstrap”) it from liquid instruments such as:

  • Deposits (e.g., overnight to 6 months)
  • Forward Rate Agreements (FRAs) and Futures
  • Swaps (e.g., 1-year to 30-year)

These instruments give us information about specific points on the curve. QuantLib uses these to construct a continuous curve using interpolation (e.g., linear, log-linear) between observed data points.

Some essential building blocks in QuantLib include:

  • RateHelpers: Abstractions that turn market quotes (like deposit or swap rates) into bootstrapping constraints.
  • DayCount conventions and Calendars: Needed for accurate date and interest calculations.
  • YieldTermStructure: The central object representing the term structure of interest rates, which you can query for zero rates, discount factors, or forward rates.

QuantLib lets you define all of this in a modular way, so you can plug in market data and generate an accurate, arbitrage-free curve for pricing and risk analysis.

3. Implement in C++ with Quantlib

To construct a yield curve in QuantLib, you’ll typically bootstrap it from a mix of deposit rates and swaps. QuantLib provides a flexible interface to handle real-world instruments and interpolate the curve. Below is a minimal C++ example that constructs a USD zero curve from deposit and swap rates:

#include <ql/quantlib.hpp>
#include <iostream>

using namespace QuantLib;

int main() {
    Calendar calendar = UnitedStates(UnitedStates::GovernmentBond);
    Date today(25, June, 2025);
    Settings::instance().evaluationDate() = today;

    // Market quotes
    Rate depositRate = 0.015; // 1.5% for 3M deposit
    Rate swapRate5Y = 0.025;  // 2.5% for 5Y swap

    // Quote handles
    Handle<Quote> dRate(boost::make_shared<SimpleQuote>(depositRate));
    Handle<Quote> sRate(boost::make_shared<SimpleQuote>(swapRate5Y));

    // Day counters and conventions
    DayCounter depositDayCount = Actual360();
    DayCounter curveDayCount = Actual365Fixed();
    Thirty360 swapDayCount(Thirty360::BondBasis);

    // Instrument helpers
    std::vector<boost::shared_ptr<RateHelper>> instruments;

    instruments.push_back(boost::make_shared<DepositRateHelper>(
        dRate, 3 * Months, 2, calendar, ModifiedFollowing, false, depositDayCount));

    instruments.push_back(boost::make_shared<SwapRateHelper>(
        sRate, 5 * Years, calendar, Annual, Unadjusted, swapDayCount,
        boost::make_shared<Euribor6M>()));

    // Construct the yield curve
    boost::shared_ptr<YieldTermStructure> yieldCurve =
        boost::make_shared<PiecewiseYieldCurve<ZeroYield, Linear>>(
            today, instruments, curveDayCount);

    // Example output: discount factor at 2Y
    Date maturity = calendar.advance(today, 2, Years);
    std::cout << "==== Yield Curve ====" << std::endl;
    for (int y = 1; y <= 5; ++y) {
    Date maturity = calendar.advance(today, y, Years);
    double discount = yieldCurve->discount(maturity);
    double zeroRate = yieldCurve->zeroRate(maturity, curveDayCount, Compounded, Annual).rate();

    std::cout << "Maturity: " << y << "Y\t"
              << "Yield: " << std::fixed << std::setprecision(2) << 100 * zeroRate << "%\t"
              << "Discount: " << std::setprecision(5) << discount
              << std::endl;
    }

    return 0;
}

This snippet shows how to:

  • Set up today’s date and calendar,
  • Define deposit and swap instruments as bootstrapping inputs,
  • Build a PiecewiseYieldCurve using QuantLib’s helper classes,
  • Query the curve for a discount factor.

You can easily extend this with more instruments (FRA, futures, longer swaps) for a realistic curve.

4. Compile, Execute and Plot

After installing quantlib, I compile my code with the following CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(QuantLibImpliedVolExample)

set(CMAKE_CXX_STANDARD 17)

find_package(PkgConfig REQUIRED)
pkg_check_modules(QUANTLIB REQUIRED QuantLib)

include_directories(${QUANTLIB_INCLUDE_DIRS})
link_directories(${QUANTLIB_LIBRARY_DIRS})

add_executable(yieldcurve ../yieldcurve.cpp)
target_link_libraries(yieldcurve ${QUANTLIB_LIBRARIES})

I run:

mkdir build
cd build
cmake ..
make

And run:

➜ build ./yieldcurve
==== Yield Curve ====
Maturity: 1Y Yield: 1.68% Discount: 0.98345
Maturity: 2Y Yield: 1.89% Discount: 0.96324
Maturity: 3Y Yield: 2.10% Discount: 0.93946
Maturity: 4Y Yield: 2.31% Discount: 0.91272
Maturity: 5Y Yield: 2.52% Discount: 0.88306

And we can plot it too:

So this is the plot of the zero yield curve from 1 to 5 years, based on your QuantLib output. It shows a smooth, gently upward-sloping curve: typical for a healthy interest rate environment.

But why is it so… Linear?

This is due to both the limited number of inputs and the interpolation method applied.

In the example above, the curve is built using QuantLib’s PiecewiseYieldCurve<ZeroYield, Linear>, which performs linear interpolation between the zero rates of the provided instruments. With only a short-term and a long-term point, this leads to a straight-line interpolation between the two, hence the linear shape.

In reality, yield curves typically exhibit curvature:

  • They rise steeply in the short term,
  • Then gradually flatten as maturity increases.

This reflects the market’s expectations about interest rates, inflation, and economic growth over time.

To better approximate real-world behavior, the curve can be constructed using:

  • A richer set of instruments (e.g., deposits, futures, swaps across many maturities),
  • More appropriate interpolation techniques such as LogLinear or Cubic.

5. A More Realistic Curve

This version uses LogLinear interpolation, which interpolates on the log of the discount factors. It results in a more natural term structure than simple linear interpolation, even with only two instruments.

For a truly realistic curve, additional instruments should be incorporated, especially swaps with intermediate maturities:

#include <ql/quantlib.hpp>
#include <iostream>
#include <iomanip>

using namespace QuantLib;

int main() {
    Calendar calendar = UnitedStates(UnitedStates::GovernmentBond);
    Date today(25, June, 2025);
    Settings::instance().evaluationDate() = today;

    // Market quotes
    Rate depositRate = 0.015;    // 1.5% for 3M deposit
    Rate swapRate2Y = 0.020;     // 2.0% for 2Y swap
    Rate swapRate5Y = 0.025;     // 2.5% for 5Y swap
    Rate swapRate10Y = 0.030;    // 3.0% for 10Y swap

    // Quote handles
    Handle<Quote> dRate(boost::make_shared<SimpleQuote>(depositRate));
    Handle<Quote> sRate2Y(boost::make_shared<SimpleQuote>(swapRate2Y));
    Handle<Quote> sRate5Y(boost::make_shared<SimpleQuote>(swapRate5Y));
    Handle<Quote> sRate10Y(boost::make_shared<SimpleQuote>(swapRate10Y));

    // Day counters and conventions
    DayCounter depositDayCount = Actual360();
    DayCounter curveDayCount = Actual365Fixed();
    Thirty360 swapDayCount(Thirty360::BondBasis);

    // Instrument helpers
    std::vector<boost::shared_ptr<RateHelper>> instruments;

    instruments.push_back(boost::make_shared<DepositRateHelper>(
        dRate, 3 * Months, 2, calendar, ModifiedFollowing, false, depositDayCount));

    instruments.push_back(boost::make_shared<SwapRateHelper>(
        sRate2Y, 2 * Years, calendar, Annual, Unadjusted, swapDayCount,
        boost::make_shared<Euribor6M>()));

    instruments.push_back(boost::make_shared<SwapRateHelper>(
        sRate5Y, 5 * Years, calendar, Annual, Unadjusted, swapDayCount,
        boost::make_shared<Euribor6M>()));

    instruments.push_back(boost::make_shared<SwapRateHelper>(
        sRate10Y, 10 * Years, calendar, Annual, Unadjusted, swapDayCount,
        boost::make_shared<Euribor6M>()));

    // Construct the yield curve with Cubic interpolation
    boost::shared_ptr<YieldTermStructure> yieldCurve =
        boost::make_shared<PiecewiseYieldCurve<ZeroYield, Cubic>>(
            today, instruments, curveDayCount);

    // Output: yield curve at each year from 1Y to 10Y
    std::cout << "==== Yield Curve ====" << std::endl;
    for (int y = 1; y <= 10; ++y) {
        Date maturity = calendar.advance(today, y, Years);
        double discount = yieldCurve->discount(maturity);
        double zeroRate = yieldCurve->zeroRate(maturity, curveDayCount, Compounded, Annual).rate();

        std::cout << "Maturity: " << y << "Y\t"
                  << "Yield: " << std::fixed << std::setprecision(2) << 100 * zeroRate << "%\t"
                  << "Discount: " << std::setprecision(5) << discount
                  << std::endl;
    }

    return 0;
}

And let’s run it again:

➜  build ./yieldcurve
==== Yield Curve ====
Maturity: 1Y    Yield: 1.67%    Discount: 0.98355
Maturity: 2Y    Yield: 2.00%    Discount: 0.96122
Maturity: 3Y    Yield: 2.20%    Discount: 0.93680
Maturity: 4Y    Yield: 2.37%    Discount: 0.91058
Maturity: 5Y    Yield: 2.51%    Discount: 0.88320
Maturity: 6Y    Yield: 2.64%    Discount: 0.85524
Maturity: 7Y    Yield: 2.75%    Discount: 0.82674
Maturity: 8Y    Yield: 2.86%    Discount: 0.79790
Maturity: 9Y    Yield: 2.96%    Discount: 0.76912
Maturity: 10Y   Yield: 3.05%    Discount: 0.74018

Which gives a better looking yield curve:

You may also like