- 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