"With an anxiety that almost amounted to agony, I collected the instruments of life around me, that I might infuse a spark of being into the lifeless thing that lay at my feet. It was already one in the morning; the rain pattered dismally against the panes, and my candle was nearly out, when, by the glimmer of the half-extinguished light, I saw the dull yellow eye of the creature open; it breathed hard, and a convulsive motion agitated its limbs."
- Mary Shelley, Frankenstein
This technical paper, which was published in
Wilmott September magazine, is the second in a series of
two on the design of software systems in computational finance. We created multi-language application to value an Equity-linked product. The actual pricing of this product was performed by using
QuantLib Monte Carlo framework. We used C++/CLI code as a wrapper class for native QuantLib C++ code. Then, we used C# as a front end to C++/CLI wrapper, after having constructed transaction-related parameters and market data. For flexible input data construction, a specific factory mechanism was implemented by using C# Assembly, Reflection API, and Dynamic data types. Finally, we interfaced C# client code to Excel by using
Excel-DNA.
The entire source code for this application is presented in this blog post. Since the paper discussed how we implemented this application, we feel that being able to compile and run the actual code is necessary in order to understand the complete design. The original post for Equity-linked note implementation (including the original transaction term sheet) can be found in
here.
The actual paper can be found in
here. Thanks for reading this blog.
-Mike
C++ project
In Visual Studio, create a new C++/CLR Class Library project (QLWrapper).
At this point,
pre-installed and pre-built QuantLib and Boost libraries should be
available. In the case one may not have these libraries available, all required
procedures for getting this part done correctly is well presented in
here and
here. Create references to required Boost and QuantLib header files
and libraries as follows.
Next, some C++/CLI project settings needs to be modified. Disable the use of pre-compiled headers.
Update properties to suppress some specific warnings.
Optionally, update properties to suppress (almost) all the other warnings.
Next, add required C++ header and source files as presented below. Add new file for containing native C++ header file information (EquityLinkedNote.h).
// EquityLinkedNote.h
#pragma once
#include <ql/quantlib.hpp>
using namespace QuantLib;
namespace QuantLibCppNative {
// 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_;
};
}
Add new file for containing native C++ source file information (EquityLinkedNote.cpp).
// EquityLinkedNote.cpp
#include "EquityLinkedNote.h"
#include <algorithm>
namespace QuantLibCppNative {
// 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;
}
}
Update the existing file to contain managed C++ header file information (QLWrapper.h).
// QLWrapper.h
#pragma once
#include "EquityLinkedNote.h"
using namespace System;
using namespace System::Collections::Generic;
namespace QLWrapper {
// C++/CLI wrapper class
public ref class EquityLinkedNote {
public:
EquityLinkedNote(double notional, double cap, double floor, DateTime transactionDate,
int settlementDays, String^ calendar, String^ dayCountConvention, int requiredSamples,
int seed, List<DateTime>^ fixingDates, List<DateTime>^ paymentDates,
double initialFixing, double volatility, Dictionary<DateTime, double>^ curve);
!EquityLinkedNote();
~EquityLinkedNote();
double PV();
private:
// note : class members as native pointers
QuantLibCppNative::EquityLinkedNote* note;
QuantLibCppNative::EquityLinkedNoteMonteCarloPricer<PseudoRandom, Statistics>* pricer;
};
}
namespace QuantLibConversions {
// one static class for all QuantLib-related type conversions
public ref class Convert abstract sealed {
public:
// convert System.String to QuantLib.DayCounter class
static DayCounter ToDayCounter(String^ dayCounterString);
// convert System.DateTime to QuantLib.Date class
static Date ToDate(DateTime dateTime);
// convert System.String to QuantLib.Calendar class
static Calendar ToCalendar(String^ calendarString);
// convert System.String to QuantLib.BusinessDayConvention enumerator
static BusinessDayConvention ToBusinessDayConvention(String^ businessDayConventionString);
};
}
Update the existing file to contain managed C++ source file information (QLWrapper.cpp).
// QLWrapper.cpp
#pragma once
#include "QLWrapper.h"
namespace QLWrapper {
using QL = QuantLibConversions::Convert;
EquityLinkedNote::EquityLinkedNote(double notional_, double cap_, double floor_,
DateTime transactionDate_, int settlementDays_, String^ calendar_,
String^ dayCountConvention_, int requiredSamples_, int seed_,
List<DateTime>^ fixingDates_, List<DateTime>^ paymentDates_,
double initialFixing_, double volatility_, Dictionary<DateTime, double>^ curve_) {
// create QL calendar from a given string
Calendar calendar = QL::ToCalendar(calendar_);
// create QL daycounter from a given string
DayCounter dayCountConvention = QL::ToDayCounter(dayCountConvention_);
Date transactionDate = QL::ToDate(transactionDate_);
Date settlementDate = calendar.advance(transactionDate, Period(settlementDays_, Days));
Settings::instance().evaluationDate() = settlementDate;
// import fixing dates to std::vector
std::vector<Date> fixingDates;
for each (System::DateTime dt in fixingDates_) {
fixingDates.push_back(QL::ToDate(dt));
}
// import payment dates to std::vector
std::vector<Date> paymentDates;
for each (System::DateTime dt in paymentDates_) {
paymentDates.push_back(QL::ToDate(dt));
}
// create structured equity-linked note
note = new QuantLibCppNative::EquityLinkedNote(notional_, initialFixing_, fixingDates, paymentDates, cap_, floor_);
// create std::vector containers for dates and discount factors
std::vector<Date> maturityDates;
std::vector<double> discountFactors;
for each (KeyValuePair<DateTime, double> kvp in curve_) {
maturityDates.push_back(QL::ToDate(kvp.Key));
discountFactors.push_back(kvp.Value);
}
// create valuation curve from a set of given discount factors
auto riskFreeRateTermStructure = boost::make_shared<InterpolatedDiscountCurve<LogLinear>>
(maturityDates, discountFactors, dayCountConvention, calendar);
Handle<YieldTermStructure> riskFreeRateTermStructureHandle(riskFreeRateTermStructure);
// create stochastic process
Handle<Quote> initialFixingHandle(boost::shared_ptr<Quote>(new SimpleQuote(initialFixing_)));
auto volatilityQuote = boost::make_shared<SimpleQuote>(volatility_);
Handle<Quote> volatilityHandle(volatilityQuote);
Handle<BlackVolTermStructure> volatilityTermStructureHandle(boost::shared_ptr<BlackVolTermStructure>
(new BlackConstantVol(settlementDays_, calendar, volatilityHandle, dayCountConvention)));
auto process = boost::make_shared<BlackScholesProcess>(initialFixingHandle, riskFreeRateTermStructureHandle, volatilityTermStructureHandle);
// create pricer instance
pricer = new QuantLibCppNative::EquityLinkedNoteMonteCarloPricer<PseudoRandom, Statistics>
(process, riskFreeRateTermStructureHandle, false, static_cast<Size>(requiredSamples_),
Null<Real>(), static_cast<Size>(requiredSamples_), static_cast<BigNatural>(seed_));
// assign pricer to instrument
// note : cast pricer from native pointer to boost shared pointer
note->setPricingEngine(static_cast<boost::shared_ptr<
QuantLibCppNative::EquityLinkedNoteMonteCarloPricer<PseudoRandom, Statistics>>>(pricer));
}
EquityLinkedNote::!EquityLinkedNote() {
delete note;
delete pricer;
}
EquityLinkedNote::~EquityLinkedNote() {
this->!EquityLinkedNote();
}
double EquityLinkedNote::PV() {
return note->NPV();
}
}
namespace QuantLibConversions {
DayCounter Convert::ToDayCounter(String^ dayCounterString) {
if (dayCounterString->ToUpper() == "ACTUAL360") return Actual360();
if (dayCounterString->ToUpper() == "THIRTY360") return Thirty360();
if (dayCounterString->ToUpper() == "ACTUALACTUAL") return ActualActual();
if (dayCounterString->ToUpper() == "BUSINESS252") return Business252();
if (dayCounterString->ToUpper() == "ACTUAL365NOLEAP") return Actual365NoLeap();
if (dayCounterString->ToUpper() == "ACTUAL365FIXED") return Actual365Fixed();
// requested day counter not found, throw exception
throw gcnew System::Exception("undefined daycounter");
}
Date Convert::ToDate(DateTime dateTime) {
// Date constructor using Excel dateserial
return Date(dateTime.ToOADate());
}
Calendar Convert::ToCalendar(String^ calendarString) {
if (calendarString->ToUpper() == "ARGENTINA.MERVAL") return Argentina(Argentina::Market::Merval);
if (calendarString->ToUpper() == "AUSTRALIA") return Australia();
if (calendarString->ToUpper() == "BRAZIL.EXCHANGE") return Brazil(Brazil::Market::Exchange);
if (calendarString->ToUpper() == "BRAZIL.SETTLEMENT") return Brazil(Brazil::Market::Settlement);
if (calendarString->ToUpper() == "CANADA.SETTLEMENT") return Canada(Canada::Market::Settlement);
if (calendarString->ToUpper() == "CANADA.TSX") return Canada(Canada::Market::TSX);
if (calendarString->ToUpper() == "CHINA.IB") return China(China::Market::IB);
if (calendarString->ToUpper() == "CHINA.SSE") return China(China::Market::SSE);
if (calendarString->ToUpper() == "CZECHREPUBLIC.PSE") return CzechRepublic(CzechRepublic::Market::PSE);
if (calendarString->ToUpper() == "DENMARK") return Denmark();
if (calendarString->ToUpper() == "FINLAND") return Finland();
if (calendarString->ToUpper() == "GERMANY.SETTLEMENT") return Germany(Germany::Market::Settlement);
if (calendarString->ToUpper() == "GERMANY.FRANKFURTSTOCKEXCHANGE") return Germany(Germany::Market::FrankfurtStockExchange);
if (calendarString->ToUpper() == "GERMANY.XETRA") return Germany(Germany::Market::Xetra);
if (calendarString->ToUpper() == "GERMANY.EUREX") return Germany(Germany::Market::Eurex);
if (calendarString->ToUpper() == "GERMANY.EUWAX") return Germany(Germany::Market::Euwax);
if (calendarString->ToUpper() == "HONGKONG.HKEX") return HongKong(HongKong::Market::HKEx);
if (calendarString->ToUpper() == "INDIA.NSE") return India(India::Market::NSE);
if (calendarString->ToUpper() == "INDONESIA.BEJ") return Indonesia(Indonesia::Market::BEJ);
if (calendarString->ToUpper() == "INDONESIA.IDX") return Indonesia(Indonesia::Market::IDX);
if (calendarString->ToUpper() == "INDONESIA.JSX") return Indonesia(Indonesia::Market::JSX);
if (calendarString->ToUpper() == "ISRAEL.SETTLEMENT") return Israel(Israel::Market::Settlement);
if (calendarString->ToUpper() == "ISRAEL.TASE") return Israel(Israel::Market::TASE);
if (calendarString->ToUpper() == "ITALY.EXCHANGE") return Italy(Italy::Market::Exchange);
if (calendarString->ToUpper() == "ITALY.SETTLEMENT") return Italy(Italy::Market::Settlement);
if (calendarString->ToUpper() == "JAPAN") return Japan();
if (calendarString->ToUpper() == "MEXICO.BMV") return Mexico(Mexico::Market::BMV);
if (calendarString->ToUpper() == "NEWZEALAND") return NewZealand();
if (calendarString->ToUpper() == "NORWAY") return Norway();
if (calendarString->ToUpper() == "POLAND") return Poland();
if (calendarString->ToUpper() == "ROMANIA") return Romania();
if (calendarString->ToUpper() == "RUSSIA.MOEX") return Russia(Russia::Market::MOEX);
if (calendarString->ToUpper() == "RUSSIA.SETTLEMENT") return Russia(Russia::Market::Settlement);
if (calendarString->ToUpper() == "SAUDIARABIA.TADAWUL") return SaudiArabia(SaudiArabia::Market::Tadawul);
if (calendarString->ToUpper() == "SINGAPORE.SGX") return Singapore(Singapore::Market::SGX);
if (calendarString->ToUpper() == "SLOVAKIA.BSSE") return Slovakia(Slovakia::Market::BSSE);
if (calendarString->ToUpper() == "SOUTHAFRICA") return SouthAfrica();
if (calendarString->ToUpper() == "SOUTHKOREA.KRX") return SouthKorea(SouthKorea::Market::KRX);
if (calendarString->ToUpper() == "SOUTHKOREA.SETTLEMENT") return SouthKorea(SouthKorea::Market::Settlement);
if (calendarString->ToUpper() == "SWEDEN") return Sweden();
if (calendarString->ToUpper() == "SWITZERLAND") return Switzerland();
if (calendarString->ToUpper() == "TAIWAN.TSEC") return Taiwan(Taiwan::Market::TSEC);
if (calendarString->ToUpper() == "TARGET") return TARGET();
if (calendarString->ToUpper() == "TURKEY") return Turkey();
if (calendarString->ToUpper() == "UKRAINE.USE") return Ukraine(Ukraine::Market::USE);
if (calendarString->ToUpper() == "UNITEDKINGDOM.EXCHANGE") return UnitedKingdom(UnitedKingdom::Market::Exchange);
if (calendarString->ToUpper() == "UNITEDKINGDOM.METALS") return UnitedKingdom(UnitedKingdom::Market::Metals);
if (calendarString->ToUpper() == "UNITEDKINGDOM.SETTLEMENT") return UnitedKingdom(UnitedKingdom::Market::Settlement);
if (calendarString->ToUpper() == "UNITEDSTATES.GOVERNMENTBOND") return UnitedStates(UnitedStates::Market::GovernmentBond);
if (calendarString->ToUpper() == "UNITEDSTATES.LIBORIMPACT") return UnitedStates(UnitedStates::Market::LiborImpact);
if (calendarString->ToUpper() == "UNITEDSTATES.NERC") return UnitedStates(UnitedStates::Market::NERC);
if (calendarString->ToUpper() == "UNITEDSTATES.NYSE") return UnitedStates(UnitedStates::Market::NYSE);
if (calendarString->ToUpper() == "UNITEDSTATES.SETTLEMENT") return UnitedStates(UnitedStates::Market::Settlement);
// requested calendar not found, throw exception
throw gcnew System::Exception("undefined calendar");
}
BusinessDayConvention Convert::ToBusinessDayConvention(String^ businessDayConventionString) {
if (businessDayConventionString->ToUpper() == "FOLLOWING") return BusinessDayConvention::Following;
if (businessDayConventionString->ToUpper() == "HALFMONTHMODIFIEDFOLLOWING") return BusinessDayConvention::HalfMonthModifiedFollowing;
if (businessDayConventionString->ToUpper() == "MODIFIEDFOLLOWING") return BusinessDayConvention::ModifiedFollowing;
if (businessDayConventionString->ToUpper() == "MODIFIEDPRECEDING") return BusinessDayConvention::ModifiedPreceding;
if (businessDayConventionString->ToUpper() == "NEAREST") return BusinessDayConvention::Nearest;
if (businessDayConventionString->ToUpper() == "PRECEDING") return BusinessDayConvention::Preceding;
if (businessDayConventionString->ToUpper() == "UNADJUSTED") return BusinessDayConvention::Unadjusted;
// requested business day convention not found, throw exception
throw gcnew System::Exception("undefined business day convention");
}
}
After this, one should be able to build this C++ project successfully.
C# project
In the first stage, a simple console application is implemented, in order to quickly test the core C++ program from C# client program. Add new C# console project (CsClient). Then, add file containing C# tester program.
using System;
using System.Collections.Generic;
namespace CsClient {
class Program {
static void Main(string[] args) {
try {
double notional = 1000000.0;
double initialFixing = 3662.18;
double volatility = 0.16;
double cap = 0.015;
double floor = 0.0;
DateTime transactionDate = new DateTime(2017, 10, 30);
int settlementDays = 2;
string calendar = "TARGET";
string dayCountConvention = "ACTUAL360";
int requiredSamples = 1000;
int seed = 0;
List<DateTime> fixingDates = new List<DateTime>() {
new DateTime(2017, 11, 30), new DateTime(2017, 12, 30), new DateTime(2018, 1, 30),
new DateTime(2018, 2, 28), new DateTime(2018, 3, 30), new DateTime(2018, 4, 30),
new DateTime(2018, 5, 30), new DateTime(2018, 6, 30), new DateTime(2018, 7, 30),
new DateTime(2018, 8, 30), new DateTime(2018, 9, 30), new DateTime(2018, 10, 30),
new DateTime(2018, 11, 30), new DateTime(2018, 12, 30), new DateTime(2019, 1, 30),
new DateTime(2019, 2, 28), new DateTime(2019, 3, 30), new DateTime(2019, 4, 30),
new DateTime(2019, 5, 30), new DateTime(2019, 6, 30), new DateTime(2019, 7, 30),
new DateTime(2019, 8, 30), new DateTime(2019, 9, 30), new DateTime(2019, 10, 30),
new DateTime(2019, 11, 30), new DateTime(2019, 12, 30), new DateTime(2020, 1, 30),
new DateTime(2020, 2, 29), new DateTime(2020, 3, 30), new DateTime(2020, 4, 30),
new DateTime(2020, 5, 30), new DateTime(2020, 6, 30), new DateTime(2020, 7, 30),
new DateTime(2020, 8, 30), new DateTime(2020, 9, 30), new DateTime(2020, 10, 30)
};
List<DateTime> paymentDates = new List<DateTime>() {
new DateTime(2018, 10, 30), new DateTime(2019, 10, 30), new DateTime(2020, 10, 30)
};
Dictionary<DateTime, double> curve = new Dictionary<DateTime, double>() {
{ new DateTime(2017, 10, 30), 1.0 }, { new DateTime(2018, 10, 30), 0.98 },
{ new DateTime(2019, 10, 30), 0.96 }, { new DateTime(2020, 10, 30), 0.92 },
{ new DateTime(2021, 10, 30), 0.85 }
};
QLWrapper.EquityLinkedNote wrapperNote = new QLWrapper.EquityLinkedNote(notional, cap, floor, transactionDate, settlementDays,
calendar, dayCountConvention, requiredSamples, seed, fixingDates, paymentDates, initialFixing, volatility, curve);
double pv = wrapperNote.PV();
Console.WriteLine(pv.ToString());
GC.SuppressFinalize(wrapperNote);
}
catch (Exception e) {
Console.WriteLine(e.Message);
}
}
}
}
Set C# project as StartUp project and add reference to C++ project (QLWrapper).
At the moment, C# console client is using managed C++ code, which is wrapping native C++ code, which uses QuantLib C++ library. At this point, build should be successfully completed and previous C# program should return PV for equity-linked note.
Configurations and Factories
In order to get rid of all hard-coded market data and transaction parameters in previous C# client, configurations and data factories will be implemented. Next, implement the following XML configuration file. Remember to define the correct paths for XML and Access files into this configuration file.
<configurations>
<XMLFilePathName>..\EquityLinkedNote.xml</XMLFilePathName>
<connectionString>Provider=Microsoft.ACE.OLEDB.12.0;Data Source=..\Market.accdb;Jet OLEDB:Database Password=MyDbPassword;</connectionString>
</configurations>
Then, implement the following XML presentation for Equity-linked note transaction.
<EquityLinkedNote>
<notional>1000000.0</notional>
<cap>0.015</cap>
<floor>0.0</floor>
<transactionDate>2017-10-30T00:00:00</transactionDate>
<settlementDays>2</settlementDays>
<calendar>TARGET</calendar>
<dayCountConvention>ACTUAL360</dayCountConvention>
<requiredSamples>1000</requiredSamples>
<seed>0</seed>
<fixingDates>
<dateTime>2017-11-30T00:00:00</dateTime> <dateTime>2017-12-30T00:00:00</dateTime>
<dateTime>2018-01-30T00:00:00</dateTime> <dateTime>2018-02-28T00:00:00</dateTime>
<dateTime>2018-03-30T00:00:00</dateTime> <dateTime>2018-04-30T00:00:00</dateTime>
<dateTime>2018-05-30T00:00:00</dateTime> <dateTime>2018-06-30T00:00:00</dateTime>
<dateTime>2018-07-30T00:00:00</dateTime> <dateTime>2018-08-30T00:00:00</dateTime>
<dateTime>2018-09-30T00:00:00</dateTime> <dateTime>2018-10-30T00:00:00</dateTime>
<dateTime>2018-11-30T00:00:00</dateTime> <dateTime>2018-12-30T00:00:00</dateTime>
<dateTime>2019-01-30T00:00:00</dateTime> <dateTime>2019-02-28T00:00:00</dateTime>
<dateTime>2019-03-30T00:00:00</dateTime> <dateTime>2019-04-30T00:00:00</dateTime>
<dateTime>2019-05-30T00:00:00</dateTime> <dateTime>2019-06-30T00:00:00</dateTime>
<dateTime>2019-07-30T00:00:00</dateTime> <dateTime>2019-08-30T00:00:00</dateTime>
<dateTime>2019-09-30T00:00:00</dateTime> <dateTime>2019-10-30T00:00:00</dateTime>
<dateTime>2019-11-30T00:00:00</dateTime> <dateTime>2019-12-30T00:00:00</dateTime>
<dateTime>2020-01-30T00:00:00</dateTime> <dateTime>2020-02-29T00:00:00</dateTime>
<dateTime>2020-03-30T00:00:00</dateTime> <dateTime>2020-04-30T00:00:00</dateTime>
<dateTime>2020-05-30T00:00:00</dateTime> <dateTime>2020-06-30T00:00:00</dateTime>
<dateTime>2020-07-30T00:00:00</dateTime> <dateTime>2020-08-30T00:00:00</dateTime>
<dateTime>2020-09-30T00:00:00</dateTime> <dateTime>2020-10-30T00:00:00</dateTime>
</fixingDates>
<paymentDates>
<dateTime>2018-10-30T00:00:00</dateTime>
<dateTime>2019-10-30T00:00:00</dateTime>
<dateTime>2020-10-30T00:00:00</dateTime>
</paymentDates>
</EquityLinkedNote>
Next, prepare MS Access database (name: Market.accdb) for containing all required market information. For the reasons of convenience we use simple MS Access database to host market data. However, extending this code for more realistic settings (for example, SQL Server) should not pose difficulties for an experienced developer.
Add C# file containing abstraction for EquityLinkedNote and required factories for XML transaction and market data.
// EquityLinkedNote.cs
using System;
using System.Collections.Generic;
using System.Xml.Serialization;
using System.IO;
using System.Data;
using System.Data.OleDb;
namespace CsClient
{
// interface for all factory classes
public interface IFactory {
// return any type
dynamic Create();
}
// XML factory : create equity-linked note from XML file
public class XMLNoteFactory : IFactory {
public XMLNoteFactory() {
// default ctor
}
public dynamic Create() {
// de-serialize equity-linked note object from XML file
XmlSerializer serializer = new XmlSerializer(typeof(EquityLinkedNote));
string XMLFilePathName = @CsClient.configurations.SelectSingleNode("configurations/XMLFilePathName").InnerText;
EquityLinkedNote note = null;
FileStream stream = File.OpenRead(XMLFilePathName);
note = (EquityLinkedNote)serializer.Deserialize(stream);
return note;
}
}
// SQL database factory : create all required market data from database tables
public class SQLMarketFactory : IFactory {
public SQLMarketFactory() {
// default ctor
}
public dynamic Create() {
// create connection string and SQL queries
string connectionString = @CsClient.configurations.SelectSingleNode("configurations/connectionString").InnerText;
// create and open connection
OleDbConnection connection = new OleDbConnection(connectionString);
connection.Open();
// create adapter, request curve data
OleDbDataAdapter adapter = new OleDbDataAdapter("select * from Curve", connection);
DataSet dataset = new DataSet();
adapter.Fill(dataset, "curve");
// update command, request volatility
adapter.SelectCommand.CommandText = "select * from Volatility";
adapter.Fill(dataset, "volatility");
// update command, request index fixing
adapter.SelectCommand.CommandText = "select * from Fixings";
adapter.Fill(dataset, "initialFixing");
// extract data to curve dictionary
Dictionary<DateTime, double> curve = new Dictionary<DateTime, double>();
for (int i = 0; i < dataset.Tables["Curve"].Rows.Count; i++) {
curve.Add(
dataset.Tables["curve"].Rows[i].Field<DateTime>("MaturityDate"),
dataset.Tables["curve"].Rows[i].Field<double>("DiscountFactor"));
}
// extract volatility and initial fixing
double volatility = dataset.Tables["volatility"].Rows[0].Field<double>("Rate");
double initialFixing = dataset.Tables["initialFixing"].Rows[0].Field<double>("Rate");
// pack all market data into tuple
Tuple<Dictionary<DateTime, double>, double, double> market =
new Tuple<Dictionary<DateTime,double>,double,double>(curve, volatility, initialFixing);
// dispose adapter, close connection, return tuple containing market
adapter.Dispose();
connection.Close();
return market;
}
}
// Equity-linked note class : container class for term sheet parameters
public class EquityLinkedNote {
public double notional;
public double cap;
public double floor;
public DateTime transactionDate;
public int settlementDays;
public string calendar;
public string dayCountConvention;
public int requiredSamples;
public int seed;
public List<DateTime> fixingDates = null;
public List<DateTime> paymentDates = null;
EquityLinkedNote() {
// default ctor required for XML de-serialization
}
public EquityLinkedNote(double notional_, double cap_, double floor_, DateTime transactionDate_,
int settlementDays_, string calendar_, string dayCountConvention_, int requiredSamples_,
int seed_, List<DateTime> fixingDates_, List<DateTime> paymentDates_) {
notional = notional_;
cap = cap_;
floor = floor_;
transactionDate = transactionDate_;
settlementDays = settlementDays_;
calendar = calendar_;
dayCountConvention = dayCountConvention_;
requiredSamples = requiredSamples_;
seed = seed_;
fixingDates = fixingDates_;
paymentDates = paymentDates_;
}
}
}
Finally, update C# client for creating transaction parameters and market data from configured XML files. Remember to update path to configuration XML file.
// Program.cs
using System;
using System.Collections.Generic;
using System.Xml;
namespace CsClient {
public static class CsClient {
public static XmlDocument configurations = null;
private static string configurationFilePath =
@"..\configurations.xml";
private static double result = 0.0;
public static void Main() {
try {
// create configurations
configurations = new XmlDocument();
configurations.Load(configurationFilePath);
// create and use factory to create equity-linked note object from XML file
IFactory noteFactory = new XMLNoteFactory();
EquityLinkedNote note = noteFactory.Create();
// use factory to create market data from database
IFactory marketDataFactory = new SQLMarketFactory();
Tuple<Dictionary<DateTime, double>, double, double> market = marketDataFactory.Create();
Dictionary<DateTime, double> curveData = market.Item1;
double volatility = market.Item2;
double initialFixing = market.Item3;
// use C++/CLI wrapper : create equity-linked note instance
QLWrapper.EquityLinkedNote wrapperNote =
new QLWrapper.EquityLinkedNote(note.notional, note.cap, note.floor,
note.transactionDate, note.settlementDays, note.calendar, note.dayCountConvention,
note.requiredSamples, note.seed, note.fixingDates, note.paymentDates, initialFixing, volatility, curveData);
// use C++/CLI wrapper : value equity-linked note
result = wrapperNote.PV();
Console.WriteLine(result.ToString());
GC.SuppressFinalize(wrapperNote);
}
catch (Exception e) {
Console.WriteLine(e.Message);
}
}
}
}
At this point, build should be successfully completed and our new C# client program should return PV for equity-linked note. All transaction parameters and required market data can now be modified, without touching the actual program.
Excel interfacing and Factory of Factories
We will be using Excel as input-output platform for our C# client program. For interfacing task we will use Excel-DNA. Dedicated blog post on how to implement this, can be found in
here. However, for the sake of completeness, the whole procedure is also explained below on a detailed level.
First, prepare Excel GUI carefully, by following detailed instructions (range names) given in the screenshot below. For flexible transaction data construction, a specific factory mechanism is
implemented by using C# Assembly, Reflection API, and Dynamic data
types. Note, that the actual factory is created inside C# program, based on the string given in cell D2 (namespace.class: CsClient.XMLNoteFactory or CsClient.ExcelNoteFactory).
Add reference to Excel-DNA library (Project - Add reference - Browse - \\ExcelDna.Integration.dll) and click OK. This dll file is inside the distribution folder what has been downloaded from Excel-DNA website. From the properties of this reference, set
Copy Local to be
False.
Add new file as text file to project (Project - Add new item - Text file) and name it to be CsClient.dna. CopyPaste the following xml code into this file.
<DnaLibrary Name="CsClient" RuntimeVersion="v4.0">
<ExternalLibrary Path="CsClient.dll" />
</DnaLibrary>
From the properties of this dna file, set
Copy to Output Directory to be
Copy if newer.
Next, from the downloaded Excel-DNA folder (Distribution), copy ExcelDna.xll file into your project folder and rename it to be CsClient.xll. Then, add this xll file into your current project (Project - Add existing item). At this point, it might be that you do not see anything else, except cs files on this window. From drop down box on the bottom right corner of this window, select
All files and you should see ExcelInterface.xll file what we just pasted into this ExcelInterface folder. Select this file and press Add. Finally, from the properties of this xll file, set
Copy to Output Directory to be
Copy if newer. At this point, we are done with C# and Excel-DNA.
Next, update EquityLinkedNote file for containing Excel factory for Equity-linked note.
// EquityLinkedNote.cs
using System;
using System.Collections.Generic;
using System.Xml.Serialization;
using System.IO;
using System.Data;
using System.Data.OleDb;
using ExcelDna.Integration;
namespace CsClient {
// interface for all factory classes
public interface IFactory {
// return any type
dynamic Create();
}
// Excel factory : create equity-linked note from Excel named ranges
public class ExcelNoteFactory : IFactory {
public ExcelNoteFactory() {
// default ctor
}
public dynamic Create() {
// create Excel application
dynamic Excel = ExcelDnaUtil.Application;
// create all scalar parameters
double notional = (double)Excel.Range["notional"].Value2;
double cap = (double)Excel.Range["cap"].Value2;
double floor = (double)Excel.Range["floor"].Value2;
DateTime transactionDate = DateTime.FromOADate((double)Excel.Range["transactionDate"].Value2);
int settlementDays = (int)Excel.Range["settlementDays"].Value2;
string calendar = (string)Excel.Range["calendar"].Value2;
string dayCountConvention = (string)Excel.Range["dayCountConvention"].Value2;
int requiredSamples = (int)Excel.Range["requiredSamples"].Value2;
int seed = (int)Excel.Range["seed"].Value2;
// create list of fixing dates
List<DateTime> fixingDates = new List<DateTime>();
dynamic fixingDatesArray = Excel.Range["fixingDates"].Value2;
for (int i = 0; i != fixingDatesArray.GetUpperBound(0); ++i) {
fixingDates.Add(DateTime.FromOADate(Convert.ToDouble(fixingDatesArray.GetValue(i + 1, 1))));
}
// create list of payment dates
List<DateTime> paymentDates = new List<DateTime>();
dynamic paymentDatesArray = Excel.Range["paymentDates"].Value2;
for (int i = 0; i != paymentDatesArray.GetUpperBound(0); ++i) {
paymentDates.Add(DateTime.FromOADate(Convert.ToDouble(paymentDatesArray.GetValue(i + 1, 1))));
}
// create equity-linked note instance
return new EquityLinkedNote(notional, cap, floor, transactionDate, settlementDays,
calendar, dayCountConvention, requiredSamples, seed, fixingDates, paymentDates);
}
}
// XML factory : create equity-linked note from XML file
public class XMLNoteFactory : IFactory {
public XMLNoteFactory() {
// default ctor
}
public dynamic Create() {
// de-serialize equity-linked note object from XML file
XmlSerializer serializer = new XmlSerializer(typeof(EquityLinkedNote));
string XMLFilePathName = @CsClient.configurations.SelectSingleNode("configurations/XMLFilePathName").InnerText;
EquityLinkedNote note = null;
FileStream stream = File.OpenRead(XMLFilePathName);
note = (EquityLinkedNote)serializer.Deserialize(stream);
return note;
}
}
// SQL database factory : create all required market data from database tables
public class SQLMarketFactory : IFactory {
public SQLMarketFactory() {
// default ctor
}
public dynamic Create() {
// create connection string and SQL queries
string connectionString = @CsClient.configurations.SelectSingleNode("configurations/connectionString").InnerText;
// create and open connection
OleDbConnection connection = new OleDbConnection(connectionString);
connection.Open();
// create adapter, request curve data
OleDbDataAdapter adapter = new OleDbDataAdapter("select * from Curve", connection);
DataSet dataset = new DataSet();
adapter.Fill(dataset, "curve");
// update command, request volatility
adapter.SelectCommand.CommandText = "select * from Volatility";
adapter.Fill(dataset, "volatility");
// update command, request index fixing
adapter.SelectCommand.CommandText = "select * from Fixings";
adapter.Fill(dataset, "initialFixing");
// extract data to curve dictionary
Dictionary<DateTime, double> curve = new Dictionary<DateTime, double>();
for (int i = 0; i < dataset.Tables["Curve"].Rows.Count; i++) {
curve.Add(
dataset.Tables["curve"].Rows[i].Field<DateTime>("MaturityDate"),
dataset.Tables["curve"].Rows[i].Field<double>("DiscountFactor"));
}
// extract volatility and initial fixing
double volatility = dataset.Tables["volatility"].Rows[0].Field<double>("Rate");
double initialFixing = dataset.Tables["initialFixing"].Rows[0].Field<double>("Rate");
// pack all market data into tuple
Tuple<Dictionary<DateTime, double>, double, double> market =
new Tuple<Dictionary<DateTime, double>, double, double>(curve, volatility, initialFixing);
// dispose adapter, close connection, return tuple containing market
adapter.Dispose();
connection.Close();
return market;
}
}
// Equity-linked note class : container class for term sheet parameters
public class EquityLinkedNote {
public double notional;
public double cap;
public double floor;
public DateTime transactionDate;
public int settlementDays;
public string calendar;
public string dayCountConvention;
public int requiredSamples;
public int seed;
public List<DateTime> fixingDates = null;
public List<DateTime> paymentDates = null;
EquityLinkedNote() {
// default ctor required for XML de-serialization
}
public EquityLinkedNote(double notional_, double cap_, double floor_, DateTime transactionDate_,
int settlementDays_, string calendar_, string dayCountConvention_, int requiredSamples_,
int seed_, List<DateTime> fixingDates_, List<DateTime> paymentDates_) {
notional = notional_;
cap = cap_;
floor = floor_;
transactionDate = transactionDate_;
settlementDays = settlementDays_;
calendar = calendar_;
dayCountConvention = dayCountConvention_;
requiredSamples = requiredSamples_;
seed = seed_;
fixingDates = fixingDates_;
paymentDates = paymentDates_;
}
}
}
Then, update C# client program for consisting interface to Excel and factory of factories. Since we might need to use Windows Forms object in our program
(MessageBox in Catch block), we need to create reference to
System.Windows.Forms library (Project - Add reference - Assemblies -
System.Windows.Forms).
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Windows.Forms;
using System.Xml;
using ExcelDna.Integration;
namespace CsClient {
public static class CsClient {
public static XmlDocument configurations = null;
private static string configurationFilePath =
@"..\configurations.xml";
private static double result = 0.0;
public static void Run() {
try {
// create Excel
dynamic Excel = ExcelDnaUtil.Application;
// create configurations
configurations = new XmlDocument();
configurations.Load(configurationFilePath);
// get factory selection from Excel
string assemblyFile = @configurations.SelectSingleNode("configurations/assemblyFilePathName").InnerText;
string factorySelection = Excel.Range["factorySelection"].Value2;
// create and use factory to create equity-linked note object
dynamic noteFactory = LoadFactory(assemblyFile, factorySelection);
dynamic note = noteFactory.Create();
// use factory to create market data from database
IFactory marketDataFactory = new SQLMarketFactory();
Tuple<Dictionary<DateTime, double>, double, double> market = marketDataFactory.Create();
Dictionary<DateTime, double> curveData = market.Item1;
double volatility = market.Item2;
double initialFixing = market.Item3;
// use C++/CLI wrapper : create equity-linked note instance
QLWrapper.EquityLinkedNote wrapperNote =
new QLWrapper.EquityLinkedNote(note.notional, note.cap, note.floor,
note.transactionDate, note.settlementDays, note.calendar,
note.dayCountConvention, note.requiredSamples, note.seed,
note.fixingDates, note.paymentDates, initialFixing, volatility, curveData);
// use C++/CLI wrapper : value equity-linked note
result = wrapperNote.PV();
Excel.Range["pv"] = result;
GC.SuppressFinalize(wrapperNote);
}
catch (Exception e) {
MessageBox.Show(e.Message);
}
}
// Load requested factory from a given assembly
public static dynamic LoadFactory(string assemblyFile, string factoryFullName) {
dynamic factory = null;
Assembly assembly = Assembly.LoadFrom(assemblyFile);
Type[] types = assembly.GetTypes();
foreach (Type type in types) {
if (type.FullName == factoryFullName) {
factory = assembly.CreateInstance(type.FullName);
break;
}
}
return factory;
}
}
}
Next, update XML configuration file with path name to dll file, which is containing C# project assembly.
<configurations>
<assemblyFilePathName>..\CsClient.dll</assemblyFilePathName>
<XMLFilePathName>..\QLWrapper\EquityLinkedNote.xml</XMLFilePathName>
<connectionString>Provider=Microsoft.ACE.OLEDB.12.0;Data Source=..\Market.accdb;Jet OLEDB:Database Password=MyDbPassword;</connectionString>
</configurations>
Finally, change C# project type to Class Library (Project - CsClient Properties - Application - Output Type - Class Library). At this point we are done with the program part. Open the actual Excel interface workbook, which has been created earlier. Open created CsClient.xll file in the open Excel workbook. For existing action button found in the worksheet, assign macro (name: Run) which will trigger C# program execution.
In this final application, Excel is using external data sources (MS Access, XML) and C# class library, which is using managed C++ code, which is wrapping native C++ code, which uses QuantLib C++ library.
Thanks for reading this blog.
-Mike