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.
-Mike
// EquityLinkedNote.h #pragma once #include <ql/quantlib.hpp> using namespace QuantLib; // // instrument implementation for equity-linked note class EquityLinkedNote : public Instrument { public: // 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; private: 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{ public: 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> { public: 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; private: 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> { public: // 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) registerWith(process_); registerWith(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>(); } } private: // 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(path.at(i) / path.at(i - 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; paymentDateCounter++; } } 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); note->setPricingEngine(engine); std::cout << note->NPV() << std::endl; } catch (std::exception& e) { std::cout << e.what() << std::endl; } return 0; }
Excel
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.