Friday, December 29, 2017

QuantLib : implementing Equity-linked note using Monte Carlo framework

Within the last post, an implementation for a simple custom instrument and analytical pricing engine was presented. This post is presenting a bit more complex implementation for an equity-linked note, using custom pricing engine implementation built on the top of QuantLib Monte Carlo framework.

Term sheet

The most essential transaction details have been presented within the following screenshot below.

In a nutshell, 3-year holding period has been divided into three annual periods. For each period, running cumulative (but capped) coupon will be calculated based on simulated index fixing values (Geometric Brownian Motion). At the end of each period, period payoff (floored) will be calculated. Pricing-wise, this period payoff will then be discounted to present valuation date. Structure total PV is the sum of all discounted period payoffs.

In order to get more familiar with QuantLib Monte Carlo framework, one may take a look at Implementing QuantLib blog or get the actual book on QuantLib implementation from Leanpub. Also, MoneyScience is organizing Introduction to QuantLib Development course regularly. The most essential parts of the library are thoroughly covered during this 3-day training course, hosted by QuantLib lead developer Luigi Ballabio. Thanks for reading this blog. Merry Christmas and Happy New Year for everybody.


// EquityLinkedNote.h
#pragma once
#include <ql/quantlib.hpp>
using namespace QuantLib;
// instrument implementation for equity-linked note
class EquityLinkedNote : public Instrument {
 // forward class declarations
 class arguments;
 class engine;
 // ctor and implementations for required base class methods
 EquityLinkedNote(Real notional, Real initialFixing, const std::vector<Date>& fixingDates, 
  const std::vector<Date>& paymentDates, Real cap, Real floor);
 bool isExpired() const;
 void setupArguments(PricingEngine::arguments* args) const;
 // term sheet information
 Real notional_;
 Real initialFixing_;
 std::vector<Date> fixingDates_;
 std::vector<Date> paymentDates_;
 Real cap_;
 Real floor_;
// inner arguments class
class EquityLinkedNote::arguments : public PricingEngine::arguments{
 void validate() const;
 Real notional;
 Real initialFixing;
 std::vector<Date> fixingDates;
 std::vector<Date> paymentDates;
 Real cap;
 Real floor;
// inner engine class
class EquityLinkedNote::engine
 : public GenericEngine<EquityLinkedNote::arguments, EquityLinkedNote::results> {
 // base class for all further engine implementations
// path pricer implementation for equity-linked note
class EquityLinkedNotePathPricer : public PathPricer<Path> {
 EquityLinkedNotePathPricer(Real notional, Real initialFixing, const std::vector<Date>& fixingDates,
  const std::vector<Date>& paymentDates, Real cap, Real floor, const Handle<YieldTermStructure>& curve);
 Real operator()(const Path& path) const;
 Real notional_;
 Real initialFixing_;
 std::vector<Date> fixingDates_;
 std::vector<Date> paymentDates_;
 Real cap_;
 Real floor_;
 Handle<YieldTermStructure> curve_;
// monte carlo framework engine implementation for base engine class
template <typename RNG = PseudoRandom, typename S = Statistics>
class EquityLinkedNoteMonteCarloPricer : public EquityLinkedNote::engine, private McSimulation<SingleVariate, RNG, S> {
 // ctor
 EquityLinkedNoteMonteCarloPricer(const boost::shared_ptr<StochasticProcess>& process,
  const Handle<YieldTermStructure>& curve, bool antitheticVariate, Size requiredSamples,
  Real requiredTolerance, Size maxSamples, BigNatural seed)
  : McSimulation<SingleVariate, RNG, S>(antitheticVariate, false), process_(process), curve_(curve),
  requiredSamples_(requiredSamples), requiredTolerance_(requiredTolerance), maxSamples_(maxSamples), seed_(seed) {
   // register observer (pricer) with observables (stochastic process, curve)
 // implementation for required base engine class method
 void calculate() const {
  // the actual simulation process will be performed within the following method
  McSimulation<SingleVariate, RNG, S>::calculate(requiredTolerance_, requiredSamples_, maxSamples_);
  this->results_.value = this->mcModel_->sampleAccumulator().mean();
  if (RNG::allowsErrorEstimate) {
   this->results_.errorEstimate = this->mcModel_->sampleAccumulator().errorEstimate();
  else {
   this->results_.errorEstimate = Null<Real>();
 // type definitions
 typedef McSimulation<SingleVariate, RNG, S> simulation;
 typedef typename simulation::path_pricer_type path_pricer_type;
 typedef typename simulation::path_generator_type path_generator_type;
 // implementation for McSimulation class virtual method
 TimeGrid timeGrid() const {
  // create time grid based on a set of given index fixing dates
  Date referenceDate = curve_->referenceDate();
  DayCounter dayCounter = curve_->dayCounter();
  std::vector<Time> fixingTimes(arguments_.fixingDates.size());
  for (Size i = 0; i != fixingTimes.size(); ++i) {
   fixingTimes[i] = dayCounter.yearFraction(referenceDate, arguments_.fixingDates[i]);
  return TimeGrid(fixingTimes.begin(), fixingTimes.end());
 // implementation for McSimulation class virtual method
 boost::shared_ptr<path_generator_type> pathGenerator() const {
  // create time grid and get information concerning number of simulation steps for a path
  TimeGrid grid = timeGrid();
  Size steps = (grid.size() - 1);
  // create random sequence generator and return path generator
  typename RNG::rsg_type generator = RNG::make_sequence_generator(steps, seed_);
  return boost::make_shared<path_generator_type>(process_, grid, generator, false);
 // implementation for McSimulation class virtual method
 boost::shared_ptr<path_pricer_type> pathPricer() const {
  // create path pricer implementation for equity-linked note
  return boost::make_shared<EquityLinkedNotePathPricer>(arguments_.notional, arguments_.initialFixing, 
   arguments_.fixingDates, arguments_.paymentDates, arguments_.cap, arguments_.floor, this->curve_);
 // private data members
 boost::shared_ptr<StochasticProcess> process_;
 Handle<YieldTermStructure> curve_;
 Size requiredSamples_;
 Real requiredTolerance_;
 Size maxSamples_;
 BigNatural seed_;
// EquityLinkedNote.cpp
#include "EquityLinkedNote.h"
#include <algorithm>
// implementations for equity-linked note methods
EquityLinkedNote::EquityLinkedNote(Real notional, Real initialFixing, const std::vector<Date>& fixingDates,
 const std::vector<Date>& paymentDates, Real cap, Real floor)
 : notional_(notional), initialFixing_(initialFixing), fixingDates_(fixingDates), 
 paymentDates_(paymentDates), cap_(cap), floor_(floor) {
 // ctor
bool EquityLinkedNote::isExpired() const {
 Date valuationDate = Settings::instance().evaluationDate();
 // note is expired, if valuation date is greater than the last fixing date
 if (valuationDate > fixingDates_.back())
  return true;
 return false;
void EquityLinkedNote::setupArguments(PricingEngine::arguments* args) const {
 EquityLinkedNote::arguments* args_ = dynamic_cast<EquityLinkedNote::arguments*>(args);
 QL_REQUIRE(args_ != nullptr, "arguments casting error");
 args_->notional = notional_;
 args_->initialFixing = initialFixing_;
 args_->fixingDates = fixingDates_;
 args_->paymentDates = paymentDates_;
 args_->cap = cap_;
 args_->floor = floor_;
void EquityLinkedNote::arguments::validate() const {
 // checks for some general argument properties
 QL_REQUIRE(notional > 0.0, "notional must be greater than zero");
 QL_REQUIRE(initialFixing > 0.0, "initial fixing must be greater than zero");
 QL_REQUIRE(cap > floor, "cap must be greater than floor");
 // check for date consistency : all payment dates have to be included 
 // within a set of given fixing dates
 for (int i = 0; i != paymentDates.size(); ++i) {
  if (std::find(fixingDates.begin(), fixingDates.end(), paymentDates[i]) == fixingDates.end()) {
   QL_REQUIRE(false, "payment date has to be included within given fixing dates");
// implementations for equity-linked path pricer methods
EquityLinkedNotePathPricer::EquityLinkedNotePathPricer(Real notional, Real initialFixing, const std::vector<Date>& fixingDates,
 const std::vector<Date>& paymentDates, Real cap, Real floor, const Handle<YieldTermStructure>& curve)
 : notional_(notional), initialFixing_(initialFixing), fixingDates_(fixingDates),
 paymentDates_(paymentDates), cap_(cap), floor_(floor), curve_(curve) {
 // ctor
// the actual pricing algorithm for a simulated path is implemented in this method
Real EquityLinkedNotePathPricer::operator()(const Path& path) const {
 Real coupon = 0.0;
 Real cumulativeCoupon = 0.0;
 Real aggregatePathPayoff = 0.0;
 int paymentDateCounter = 0;
 // loop through fixing dates
 for (int i = 1; i != fixingDates_.size(); ++i) {
  // calculate floating coupon, based on simulated index fixing values
  coupon = std::min( / - 1) - 1, cap_);
  // add floating coupon to cumulative coupon
  cumulativeCoupon += coupon;
  // calculate period payoff for each payment date
  if (fixingDates_[i] == paymentDates_[paymentDateCounter]) {
   // calculate discounted payoff for current period, add value to aggregate path payoff
   aggregatePathPayoff += std::max(cumulativeCoupon, floor_) * notional_ * curve_->discount(fixingDates_[i]);
   // re-initialize cumulative coupon to zero, look for the next payment date
   cumulativeCoupon = 0.0;
 return aggregatePathPayoff;
// main.cpp
#include "EquityLinkedNote.h"
int main() {
 try {
  // common data : calendar, daycounter, dates
  Calendar calendar = TARGET();
  DayCounter dayCounter = Actual360();
  Date transactionDate(30, October, 2017);
  Natural settlementDays = 2;
  Date settlementDate = calendar.advance(transactionDate, Period(settlementDays, Days));
  Settings::instance().evaluationDate() = settlementDate;
  // term sheet parameters
  Real notional = 1000000.0;
  Real initialFixing = 3662.18;
  Real cap = 0.015;
  Real floor = 0.0;
  std::vector<Date> fixingDates {
   Date(30, November, 2017), Date(30, December, 2017), Date(30, January, 2018), 
   Date(28, February, 2018), Date(30, March, 2018), Date(30, April, 2018),
   Date(30, May, 2018), Date(30, June, 2018), Date(30, July, 2018), 
   Date(30, August, 2018), Date(30, September, 2018), Date(30, October, 2018),
   Date(30, November, 2018), Date(30, December, 2018), Date(30, January, 2019), 
   Date(28, February, 2019), Date(30, March, 2019), Date(30, April, 2019),
   Date(30, May, 2019), Date(30, June, 2019), Date(30, July, 2019), 
   Date(30, August, 2019), Date(30, September, 2019), Date(30, October, 2019),
   Date(30, November, 2019), Date(30, December, 2019), Date(30, January, 2020), 
   Date(29, February, 2020), Date(30, March, 2020), Date(30, April, 2020),
   Date(30, May, 2020), Date(30, June, 2020), Date(30, July, 2020), 
   Date(30, August, 2020), Date(30, September, 2020), Date(30, October, 2020)
  std::vector<Date> paymentDates {
   Date(30, October, 2018), Date(30, October, 2019), Date(30, October, 2020)
  // create structured equity-linked note
  auto note = boost::make_shared<EquityLinkedNote>(notional, initialFixing, fixingDates, paymentDates, cap, floor);
  // market data
  // create discount curve
  Real riskFreeRate = 0.01;
  auto riskFreeRateQuote = boost::make_shared<SimpleQuote>(riskFreeRate);
  Handle<Quote> riskFreeRateHandle(riskFreeRateQuote);
  auto riskFreeRateTermStructure = boost::make_shared<FlatForward>(settlementDate, riskFreeRateHandle, dayCounter);
  Handle<YieldTermStructure> riskFreeRateTermStructureHandle(riskFreeRateTermStructure);
  // create stochastic process
  Handle<Quote> initialFixingHandle(boost::shared_ptr<Quote>(new SimpleQuote(initialFixing)));
  Real volatility = 0.16;
  auto volatilityQuote = boost::make_shared<SimpleQuote>(volatility);
  Handle<Quote> volatilityHandle(volatilityQuote);
  Handle<BlackVolTermStructure> volatilityTermStructureHandle(boost::shared_ptr<BlackVolTermStructure>
   (new BlackConstantVol(settlementDays, calendar, volatilityHandle, dayCounter)));
  auto process = boost::make_shared<BlackScholesProcess>(initialFixingHandle, riskFreeRateTermStructureHandle, volatilityTermStructureHandle);
  // create simulation-related attributes
  bool useAntitheticVariates = false;
  Size requiredSamples = 1000;
  Real requiredTolerance = Null<Real>();
  Size maxSamples = 1000;
  BigNatural seed = 0;
  auto engine = boost::make_shared<EquityLinkedNoteMonteCarloPricer<PseudoRandom, Statistics>>
   (process, riskFreeRateTermStructureHandle, useAntitheticVariates, requiredSamples, requiredTolerance, maxSamples, seed);
  std::cout << note->NPV() << std::endl;
 catch (std::exception& e) {
  std::cout << e.what() << std::endl;
 return 0;


The following screenshots are presenting parameters, result, path simulations and coupon calculations in Excel for this structured note. PV is an average of all discounted path payoffs.

No comments:

Post a Comment