Table of Contents

Migrating from QuantLib's C++

nanoql's C++ layer, nanoql.hpp, keeps QuantLib's class names and member names, so most QuantLib code maps class by class: the same Date, MakeVanillaSwap or DiscountingSwapEngine, and the same discount(t) or fairRate(). What changes is how they are connected, and the C++ around them: objects held by value rather than by shared pointer, no handles or quotes, and indexes and day counters taken by name from a convention set. This guide pairs QuantLib's C++ with nanoql's for the rates products, each pair building the same trade on the same quotes. The C++ page gives the install, the CMake package and a first program.

Each QuantLib block below follows #include <ql/quantlib.hpp> and using namespace QuantLib;, each nanoql block #include "nanoql.hpp" and using namespace nanoql;, both with #include <cstdio> for std::printf, and each runs in main() after the blocks above it.

The concepts

QuantLib nanoql
Settings::instance().evaluationDate() = today; EvaluationContext context(today);, passed to each call that reads it
SimpleQuote, Handle<Quote> a double: a new quote means a new object, built from it
Handle<YieldTermStructure>, RelinkableHandle<YieldTermStructure> the curve itself, passed where it is used
ext::shared_ptr<T>, ext::make_shared<T>(…) T by value: each object holds a handle to its nanoql object, a copy takes another handle to the same object, and the destructor releases its own
instrument->setPricingEngine(engine), then instrument->NPV() Engine::calculate(instrument, context, …), returning a results object whose values are std::optional<double>
observers and LazyObject the application's layer above the library: rebuild on change, or memoize by inputs (the stateless design)
index->addFixing(date, value), held in a global store context.fixings().addFixing(index, date, value), in a new context: context.withFixings(…)
ext::make_shared<Euribor6M>(h), Actual365Fixed(), TARGET() by name from a convention set: qs.iborIndex("Euribor6M", curve), qs.dayCounter("Actual365Fixed"), qs.calendar("TARGET"), with ConventionSet qs = ConventionSet::quantLib(); (conventions as data)
PiecewiseYieldCurve<Discount, LogLinear> PiecewiseLogLinearDiscount, QuantLib-Python's name for it
MakeVanillaSwap(5 * Years, index, 0.025).withNominal(10e6), converted to the swap MakeVanillaSwap(Period(5, TimeUnit::Years), index), then .withFixedRate(0.025), .withNominal(10e6) and .toVanillaSwap(context)
Date(15, October, 2026), 5 * Years Date(15, Month::October, 2026), Period(5, TimeUnit::Years)
QuantLib::Error, from QL_REQUIRE nanoql::error, a std::runtime_error with the C interface's code and the parameter it names

Curves

// QuantLib
Date today(15, October, 2026);
Settings::instance().evaluationDate() = today;

auto quote = [](Rate r) { return Handle<Quote>(ext::make_shared<SimpleQuote>(r)); };

std::vector<std::pair<Integer, Rate>> swapQuotes = {
    {2, 0.0225}, {3, 0.0232}, {5, 0.0245}, {7, 0.0256},
    {10, 0.0270}, {15, 0.0283}, {20, 0.0288}, {30, 0.0285}};
std::vector<ext::shared_ptr<RateHelper>> helpers = {
    ext::make_shared<DepositRateHelper>(quote(0.0205), ext::make_shared<Euribor>(3 * Months)),
    ext::make_shared<DepositRateHelper>(quote(0.0210), ext::make_shared<Euribor6M>()),
};
for (auto [years, rate] : swapQuotes) {
    auto index = ext::make_shared<EuriborSwapIsdaFixA>(years * Years);
    helpers.push_back(ext::make_shared<SwapRateHelper>(quote(rate), index));
}
auto curve = ext::make_shared<PiecewiseYieldCurve<Discount, LogLinear>>(
    today, helpers, Actual365Fixed());
Handle<YieldTermStructure> curveHandle(curve);
// nanoql
Date today(15, Month::October, 2026);
EvaluationContext context(today);
ConventionSet qs = ConventionSet::quantLib();
DayCounter actual365 = qs.dayCounter("Actual365Fixed");

std::vector<std::pair<int, double>> swapQuotes = {
    {2, 0.0225}, {3, 0.0232}, {5, 0.0245}, {7, 0.0256},
    {10, 0.0270}, {15, 0.0283}, {20, 0.0288}, {30, 0.0285}};
std::vector<BootstrapHelper_YieldTermStructure> helpers = {
    DepositRateHelper(0.0205, qs.iborIndex("Euribor", Period(3, TimeUnit::Months)), context),
    DepositRateHelper(0.0210, qs.iborIndex("Euribor6M"), context),
};
for (auto [years, rate] : swapQuotes) {
    SwapIndex index = qs.swapIndex("EuriborSwapIsdaFixA", Period(years, TimeUnit::Years));
    helpers.push_back(SwapRateHelper(rate, index, context));
}
PiecewiseLogLinearDiscount curve(today, helpers, actual365, context);

The helpers take their quotes as numbers and the context, the indexes come from the convention set, and the curve is passed as itself: there is no handle. The vector holds the helpers as BootstrapHelper_YieldTermStructure, the type the curve takes, which RateHelper derives from.

Indexes and fixings

// QuantLib
auto euribor = ext::make_shared<Euribor6M>(curveHandle);
Date yesterday = euribor->fixingCalendar().advance(today, -1, Days);
euribor->addFixing(yesterday, 0.0212);
std::printf("%.4f\n", euribor->fixing(yesterday));  // 0.0212
// nanoql
IborIndex euribor = qs.iborIndex("Euribor6M", curve);
Date yesterday = euribor.fixingCalendar().advance(today, -1, TimeUnit::Days);
context = context.withFixings(context.fixings().addFixing(euribor, yesterday, 0.0212));
std::printf("%.4f\n", euribor.fixing(yesterday, context));  // 0.0212

QuantLib's fixing goes into a global store every Euribor 6M index reads; nanoql's goes into a new context, which the variable now holds and each later call takes.

Vanilla swaps

// QuantLib
auto engine = ext::make_shared<DiscountingSwapEngine>(curveHandle);
ext::shared_ptr<VanillaSwap> swap = MakeVanillaSwap(5 * Years, euribor, 0.025)
    .withNominal(10e6)
    .withPricingEngine(engine);
std::printf("%.2f %.6f\n", swap->NPV(), swap->fairRate());  // -23323.32 0.024500
// nanoql
VanillaSwap swap = MakeVanillaSwap(Period(5, TimeUnit::Years), euribor)
    .withFixedRate(0.025)
    .withNominal(10e6)
    .toVanillaSwap(context);
FixedVsFloatingSwapResults result = DiscountingSwapEngine::calculate(swap, context, curve);
std::printf("%.2f %.6f\n", *result.NPV(), *result.fairRate());  // -23323.32 0.024500

The builder ends in toVanillaSwap(context), and the engine is a function of the swap, the context and the curve.

Overnight swaps

// QuantLib
Handle<YieldTermStructure> flat(
    ext::make_shared<FlatForward>(today, 0.036, Actual365Fixed()));
auto sofr = ext::make_shared<Sofr>(flat);
ext::shared_ptr<OvernightIndexedSwap> ois = MakeOIS(5 * Years, sofr, 0.036)
    .withNominal(10e6)
    .withPricingEngine(ext::make_shared<DiscountingSwapEngine>(flat));
std::printf("%.2f %.6f\n", ois->NPV(), ois->fairRate());  // 7041.24 0.036154
// nanoql
FlatForward flat(today, 0.036, actual365);
OvernightIndex sofr = qs.overnightIndex("Sofr", flat);
OvernightIndexedSwap ois = MakeOIS(Period(5, TimeUnit::Years), sofr)
    .withFixedRate(0.036)
    .withNominal(10e6)
    .toOvernightIndexedSwap(context);
OvernightIndexedSwapResults oisResult =
    DiscountingSwapEngine::calculate(ois, context, flat);
std::printf("%.2f %.6f\n", *oisResult.NPV(), *oisResult.fairRate());  // 7041.24 0.036154

Caps and floors

// QuantLib
ext::shared_ptr<CapFloor> cap = MakeCapFloor(CapFloor::Cap, 5 * Years, euribor, 0.025)
    .withNominal(10e6);
cap->setPricingEngine(ext::make_shared<BlackCapFloorEngine>(curveHandle, 0.20));
std::printf("%.2f\n", cap->NPV());  // 117911.83
// nanoql
CapFloor cap = MakeCapFloor(CapFloorType::Cap, Period(5, TimeUnit::Years), euribor)
    .withStrike(0.025)
    .withNominal(10e6)
    .toCapFloor(context);
InstrumentResults capResult = BlackCapFloorEngine::calculate(cap, context, curve, 0.20);
std::printf("%.2f\n", *capResult.NPV());  // 117911.83

Swaptions

// QuantLib
auto swapIndex = ext::make_shared<EuriborSwapIsdaFixA>(5 * Years, curveHandle);
ext::shared_ptr<Swaption> swaption = MakeSwaption(swapIndex, 1 * Years, 0.025)
    .withNominal(10e6);
swaption->setPricingEngine(ext::make_shared<BlackSwaptionEngine>(curveHandle, 0.20));
std::printf("%.2f\n", swaption->NPV());  // 111256.85
// nanoql
SwapIndex swapIndex =
    qs.swapIndex("EuriborSwapIsdaFixA", Period(5, TimeUnit::Years), curve);
Swaption swaption = MakeSwaption(swapIndex, Period(1, TimeUnit::Years), 0.025)
    .withNominal(10e6)
    .toSwaption(context);
InstrumentResults swaptionResult =
    BlackSwaptionEngine::calculate(swaption, context, curve, 0.20);
std::printf("%.2f\n", *swaptionResult.NPV());  // 111256.85

Each pair prints the same values, shown in its comments. The rates guides show nanoql's for the same trades (swaps, caps and floors, swaptions), and getting started its overnight swap.