Sunday, April 22, 2018

C++/CLI Interoperability : Using QuantLib in C#

Assume the following scenario : there is QuantLib program available, written in native C++ and you would like to use it from your C# program. What would you do? Now, even there are several extensions available, in this post we will implement specific scheme of using native C++ program in C# via C++/CLI wrapper program. The scheme presented here is widely known as Adapter design pattern.

Our native C++ program is using QuantLib libraries for creating custom implementations for instrument and pricing engine (zero-coupon bond). C++/CLI is then wrapping this native C++ program and exposing only selected methods for its client (C# program). For those completely new to C++/CLI language, there is a relatively comprehensive tutorial available here, written by Adam Sawicki. Needless to say, even this post is presenting relatively simple example of using QuantLib via wrapper, one is able to apply presented scheme to much more complex valuation scenarios. Sounds like a cliche, but "only the sky is the limit" applies well in here.

As far as I see, a programmer is facing complexities here on two fronts : on a pure program level (C++ language, QuantLib library) and on technical level (handling projects and their configurations specifically on Visual Studio). I assume, that the reader is already familiar with C++ language (including its modern features) and has experience on using QuantLib library. Now, in order to decrease the learning curve on that technical level, I decided to include all possible configurations-related steps in here. This means, that when all the steps described in this post have been implemented on a priestly manner, one should end up with succesfully compiled projects. In order to give some concrete back-up for this claim, I have specifically re-created this project succesfully from the scratch now two times.


C++/CLI wrapper project


Create a new C++ CRL Class library project. At this point, pre-installed and pre-built QuantLib and Boost libraries should be available. Create references to required Boost and QuantLib header files and libraries as follows.









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.

Next, add the following two new header files to this project.

Native C++ header file.

// MJZeroCouponBond.h
#pragma once
#include <ql/quantlib.hpp>
using namespace QuantLib;

namespace QuantLibCppNative {

 // implementation for zero-coupon bond instrument
 class MJZeroCouponBond : public Instrument {
 public:
  // forward class declarations
  class arguments;
  class engine;
  //
  // ctor and implementations for required base class methods
  MJZeroCouponBond(Real faceAmount, Date maturityDate);
  bool isExpired() const;
 private:
  void setupArguments(PricingEngine::arguments* args) const;
  // term sheet related information
  Real faceAmount_;
  Date maturityDate_;
 };

 // inner arguments class
 class MJZeroCouponBond::arguments : public PricingEngine::arguments{
 public:
  void validate() const;
  Real faceAmount;
  Date maturityDate;
 };

 // inner engine class
 class MJZeroCouponBond::engine
  : public GenericEngine < MJZeroCouponBond::arguments, MJZeroCouponBond::results > {
  // base class for all further engine implementations
 };

 // implementation for base class engine
 class MJZeroCouponBondEngine : public MJZeroCouponBond::engine {
 public:
  MJZeroCouponBondEngine(const Handle<YieldTermStructure>& curve);
  void calculate() const;
 private:
  Handle<YieldTermStructure> curve_;
 };

}

C++/CLI header file.

// Wrapper.h
#pragma once
#include "MJZeroCouponBond.h"
using namespace System;

namespace QuantLibCppWrapper {

 // C++/CLI wrapper class for native C++ QuantLib
 public ref class MJZeroCouponBondWrapper {
 public:
  MJZeroCouponBondWrapper(double riskFreeRate_, double faceAmount_, DateTime transactionDate_,
   DateTime maturityDate_, String^ calendar_, String^ daycounter_, int settlementDays_);
  !MJZeroCouponBondWrapper();
  ~MJZeroCouponBondWrapper();
  double PV();
 private:
  // note : class members as native pointers
  QuantLibCppNative::MJZeroCouponBond* bond;
  QuantLibCppNative::MJZeroCouponBondEngine* pricer;
 };
}

Next, add the following two implementation files to this project.

Native C++ implementation file.

// MJZeroCouponBond.cpp
#include "MJZeroCouponBond.h"
using namespace QuantLib;

namespace QuantLibCppNative {

 // implementations for MJZeroCouponBond instrument
 MJZeroCouponBond::MJZeroCouponBond(Real faceAmount, Date maturityDate)
  : faceAmount_(faceAmount), maturityDate_(maturityDate) { // ctor
 }

 bool MJZeroCouponBond::isExpired() const {
  return Settings::instance().evaluationDate() >= maturityDate_;
 }

 void MJZeroCouponBond::setupArguments(PricingEngine::arguments* args) const {
  MJZeroCouponBond::arguments* args_ = dynamic_cast<MJZeroCouponBond::arguments*>(args);
  QL_REQUIRE(args_ != nullptr, "arguments casting error");
  args_->faceAmount = faceAmount_;
  args_->maturityDate = maturityDate_;
 }

 void MJZeroCouponBond::arguments::validate() const {
  QL_REQUIRE(faceAmount > 0.0, "transaction face amount cannot be zero");
 }

 // implementations for MJZeroCouponBondEngine
 MJZeroCouponBondEngine::MJZeroCouponBondEngine(const Handle<YieldTermStructure>& curve)
  : curve_(curve) {
  // register observer (MJZeroCouponBondEngine) with observable (curve)
  registerWith(curve_);
 }

 void MJZeroCouponBondEngine::calculate() const {
  // extract required parameters from arguments or curve object
  // implement the actual pricing algorithm and store result
  Date maturityDate = arguments_.maturityDate;
  Real P = arguments_.faceAmount;
  DiscountFactor df = curve_->discount(maturityDate);
  Real PV = P * df;
  results_.value = PV;
 }

}

C++/CLI implementation file.

// Wrapper.cpp
#pragma once
#include "Wrapper.h"

namespace QuantLibCppWrapper {

 // ctor
 MJZeroCouponBondWrapper::MJZeroCouponBondWrapper(double riskFreeRate_, double faceAmount_, DateTime transactionDate_,
  DateTime maturityDate_, String^ calendar_, String^ daycounter_, int settlementDays_) {

  // conversion for calendar (string to QuantLib::Calendar)
  Calendar calendar;
  if (calendar_->ToUpper() == "TARGET") calendar = TARGET();
   else throw gcnew System::Exception("undefined calendar");
  
  // conversion for daycounter (string to QuantLib::DayCounter)
  DayCounter dayCounter;
  if (daycounter_->ToUpper() == "ACT360") dayCounter = Actual360();
   else throw gcnew System::Exception("undefined daycounter");

  // conversion : transaction and maturity dates (Datetime to QuantLib::Date)
  Date transactionDate(transactionDate_.ToOADate());
  Date maturityDate(maturityDate_.ToOADate());
  Date settlementDate = calendar.advance(transactionDate, Period(settlementDays_, Days));
  Settings::instance().evaluationDate() = settlementDate;

  // create flat discount curve
  auto riskFreeRate = boost::make_shared<SimpleQuote>(riskFreeRate_);
  Handle<Quote> riskFreeRateHandle(riskFreeRate);
  auto riskFreeRateTermStructure = boost::make_shared<FlatForward>(settlementDate, riskFreeRateHandle, dayCounter);
  Handle<YieldTermStructure> riskFreeRateTermStructureHandle(riskFreeRateTermStructure);

  // create instrument and pricer as native pointers
  bond = new QuantLibCppNative::MJZeroCouponBond(faceAmount_, maturityDate);
  pricer = new QuantLibCppNative::MJZeroCouponBondEngine(riskFreeRateTermStructureHandle);

  // pair instrument and pricer
  // note : cast pricer from native pointer to boost shared pointer
  bond->setPricingEngine(static_cast<boost::shared_ptr<QuantLibCppNative::MJZeroCouponBondEngine>>(pricer));
 }

 // finalizer
 MJZeroCouponBondWrapper::!MJZeroCouponBondWrapper() {
  delete bond;
  delete pricer;
 }

 // destructor
 MJZeroCouponBondWrapper::~MJZeroCouponBondWrapper() {
  this->!MJZeroCouponBondWrapper();
 }

 // return pv
 double MJZeroCouponBondWrapper::PV() {
  return bond->NPV();
 }

}

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.









After these steps, I have completed a succesfull built for my C++/CLI project.








C# client project


First, create a new C# console project into the existing solution.

Then, add reference to previously created C++/CLI project.








Next, implement the following program to C# project.

using System;

namespace Client {

    static class ZeroCouponBond {
        static void Main() {
            try {

                // create parameters
                double riskFreeRate = 0.01;
                double faceAmount = 1000000.0;
                DateTime transactionDate = new DateTime(2018, 4, 16);
                DateTime maturityDate = new DateTime(2020, 4, 16);
                string calendar = "TARGET";
                string daycounter = "ACT360";
                int settlementDays = 2;
                
                // use C++/CLI wrapper : create bond, request pv
                QuantLibCppWrapper.MJZeroCouponBondWrapper zero = new QuantLibCppWrapper.MJZeroCouponBondWrapper(
                    riskFreeRate, faceAmount, transactionDate, maturityDate, calendar, daycounter, settlementDays);
                double PV = zero.PV();
                Console.WriteLine(PV.ToString());
                GC.SuppressFinalize(zero);
            }
            catch (Exception e) {
                Console.WriteLine(e.Message);
            }
        }
    }
}

Finally, set C# project as start-up project (on Visual Studio, right-click selected C# project and select "Set as StartUp Project").

After completing all previous steps, I have completed a succesfull built for the both projects and got the result of 979953.65 as present value for this 2-year zero-coupon bond, evaluated by using our native C++ QuantLib program via C++/CLI wrapper class.








At this point, we are done.

Postlude : no sweeping under the carpet allowed


Clever eyes might be catching, that there is a SuppressFinalize method call made for Garbage Collector at the end of our C# program. If I will remove that call, I will get the following exception.






Now, the both destructor and finalizer in our C++/CLI project have been correctly implemented, as C++/CLI standard is suggesting. As I understand, the issue is coming from C# program side, as Garbage Collector will do its final memory release sweep before exit. The issue has been pretty completely chewed in here, here and here.

Interesting point is, that in the last given reference above, the author is actually suggesting to delete dynamically allocated member variables (bond and pricer) in destructor. Now, if I will modify that C++/CLI program accordingly as suggested (delete member variables in destructor, not in finalizer, remove trigger call from destructor to finalizer, remove method call for Garbage Collector in C# program), this program will work again as expected. By taking a look at author's past experience, at least I would come to the conclusion, that this suggested approach is also a way to go.

Finally, thanks a lot again for using your precious time for reading this blog. I hope you got what you were looking for.
-Mike

No comments:

Post a Comment