Friday, May 11, 2018

C++/CLI Interoperability : Using QuantLib in C#, part II


This post is just a recap, which summarizes the content of two previous posts, published in here and here. It is presenting one possible solution for processing XML-based transactions by using QuantLib C++ native library, via C++/CLI wrapper in C# client. In order to implement the scheme presented here, all instructions given in those above-mentioned posts should be followed carefully.

Pipeline


Even the fanciest restaurant needs to have dedicated staff to visit fresh market and peel potatoes, in order to have everything ready for head chef to prepare delicious late night dinners for us. Similarly, having a "state-of-the-art" analytics library (such as QuantLib) available for calculations is still only "halfway home", since all required data inputs (ex. transactions) need to be constructed beforehand. It is highly preferred for this part of the process to be performed in a way, that maximum re-configurability would be maintained, while manual labour would be completely avoided.

In a nutshell, this post will present, how to go from having several XML transaction configuration files (such as the one example presented below) in a directory

<ZeroCouponBond>
  <transactionType>ZeroCouponBond</transactionType>
  <transactionID>DBS.1057</transactionID>
  <faceAmount>1000000</faceAmount>
  <tradeDate>2018-05-07T00:00:00</tradeDate>
  <settlementDate>2018-05-09T00:00:00</settlementDate>
  <maturityDate>2048-12-16T00:00:00</maturityDate>
  <calendar>SINGAPORE.SGX</calendar>
  <paymentConvention>MODIFIEDFOLLOWING</paymentConvention>
</ZeroCouponBond>

to have all these transactions constructed and processed through QuantLib. Console print below is showing 31 such constructed and processed dummy zero-coupon bond transactions.























C# project


Client

The story here goes roughly as follows. Create QuantLib::FlatForward [Wrapper.QlFlatForward] curve and QuantLib::DiscountingBondEngine [Wrapper.QlDiscountingBondEngine] instances by using C++/CLI wrapper classes. After this, all transactions will be created by Builder.TransactionsBuilder from specific directory, by using XML de-serialization. Instances of QuantLib::ZeroCouponBond [Wrapper.QlZeroCouponBond] objects will be created and paired with existing pricing engine (DiscountingBondEngine). Finally, PV for each transaction will be processed by pricing engine and resulting PV attribute will be stored back to transaction. Resulting information will also being printed back to console.

namespace Client {
    
    using Wrapper;
    using Builder;
    using Library;

    static class Program {
        static void Main(string[] args) {

            try {

                // use C++/CLI wrapper : create QuantLib::FlatForward as discounting curve
                double riskFreeRate = 0.015;
                string dayCounter = "ACTUAL360";
                DateTime settlementDate = new DateTime(2018, 5, 9);
                QlFlatForward discountingCurve = new QlFlatForward(riskFreeRate, dayCounter, settlementDate);

                // use C++/CLI wrapper : create QuantLib::DiscountingBondEngine for pricing
                QlDiscountingBondEngine pricer = new QlDiscountingBondEngine(discountingCurve);

                // use builder to create transactions from directory
                var transactions = TransactionsBuilder.Build();

                // use C++/CLI wrapper : create QuantLib::ZeroCouponBond instruments to list
                var zeroCouponBonds = transactions
                    .Where(t => t.transactionType == "ZeroCouponBond")
                    .Select(t => new QlZeroCouponBond(
                        t.faceAmount, 
                        t.tradeDate, 
                        t.settlementDate,
                        t.maturityDate, 
                        t.calendar, 
                        t.paymentConvention))
                    .ToList<QlZeroCouponBond>();

                // use C++/CLI wrapper : pair created bonds with pricing engine
                zeroCouponBonds.ForEach(z => z.SetPricingEngine(pricer));

                // use C++/CLI wrapper : assign processed pv attributes to transactions
                for(int i = 0; i < zeroCouponBonds.Count; i++) {
                    transactions[i].PV = zeroCouponBonds[i].PV();
                }

                // print information on transactions
                transactions.ForEach(t => {
                    string message = String.Format("ID:{0, -15} Maturity:{1, -15} PV:{2, -10}", 
                        t.transactionID,
                        t.maturityDate.ToShortDateString(),
                        String.Format("{0:0,0}", t.PV));
                    Console.WriteLine(message);
                });

                zeroCouponBonds.ForEach(z => GC.SuppressFinalize(z));
                GC.SuppressFinalize(discountingCurve);
                GC.SuppressFinalize(pricer);

            } 
            catch (Exception e) {
                Console.WriteLine(e.Message);
            }
        }
    }

}

Library


using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml;
using System.Xml.Serialization;
 
namespace Library {

    // abstract base class for all transaction types
    // storing only properties common for all transactions
    public abstract class Transaction {

        // no de-serialization for pv attribute
        private double pv;
        public double PV { get { return pv; } set { pv = value; } }

        public string transactionType;
        public string transactionID;

        // default ctor, required for serialization
        public Transaction() {
        }

        // parameter ctor
        public Transaction(string transactionType, string transactionID) {
            this.transactionType = transactionType;
            this.transactionID = transactionID;
        }

    }

    // class for hosting zero-coupon bond term sheet information
    public class ZeroCouponBond : Transaction {

        public double faceAmount;
        public DateTime tradeDate;
        public DateTime settlementDate;
        public DateTime maturityDate;
        public string calendar;
        public string paymentConvention;

        public ZeroCouponBond()
            : base() {
            // required ctor for serialization
        }

        public ZeroCouponBond(string transactionType, string transactionID,
            double faceAmount, DateTime tradeDate, DateTime settlementDate,
            DateTime maturityDate, string calendar, string paymentConvention)
            : base(transactionType, transactionID) {

            this.faceAmount = faceAmount;
            this.tradeDate = tradeDate;
            this.settlementDate = settlementDate;
            this.maturityDate = maturityDate;
            this.calendar = calendar;
            this.paymentConvention = paymentConvention;
        }

    }

}

Builder

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml;
using System.Xml.Serialization;

namespace Builder {

    public static class SerializerFactory {
        // create generic de-serializer instance from a given transaction type string
        public static dynamic Create(string transactionType) {
            // note : required type string ("Namespace.Classname") is received without namespace string
            Type t = typeof(TransactionSerializer<>).MakeGenericType(Type.GetType(String.Format("Library.{0}", transactionType)));
            return Assembly.GetAssembly(t).CreateInstance(t.FullName);
        }
    }

    // de-serialize given xml file to transaction of type T
    public class TransactionSerializer<T> {
        public T Create(string transactionFilePathName) {
            XmlSerializer serializer = new XmlSerializer(typeof(T));
            FileStream stream = File.OpenRead(transactionFilePathName);
            return (T)serializer.Deserialize(stream);
        }
    }

    public static class TransactionsBuilder {

        // return list of transaction instances, de-serialized from xml files 
        public static List<dynamic> Build() {

            // create configurations
            // NOTE : use environmental variable in path construction
            string configurationsFilePathName =
                Path.Combine(Environment.GetEnvironmentVariable("CONFIGURATIONS").ToString(), "configurations.xml");
            XmlDocument configurations = new XmlDocument();
            configurations.Load(configurationsFilePathName);
            string transactionsDirectory =
                configurations.SelectSingleNode("Configurations/TransactionsFilePathName").InnerText.ToString();

            // create transaction file names and empty list for storing transaction instances
            string[] transactionFiles = Directory.GetFiles(transactionsDirectory);
            List<dynamic> transactions = new List<dynamic>();

            // loop through transaction file names
            foreach(string transactionFile in transactionFiles) {

                // create transaction xml file
                XmlDocument transactionXMLDocument = new XmlDocument();
                transactionXMLDocument.Load(transactionFile);

                // investigate transaction type inside file
                string transactionType = (transactionXMLDocument.DocumentElement)["transactionType"].InnerText.ToString();

                // use factory class for creating correct de-serializer
                dynamic factory = SerializerFactory.Create(transactionType);

                // use de-serializer to create transaction instance
                dynamic transaction = factory.Create(transactionFile);
                transactions.Add(transaction);
            }
            return transactions;
        }

    }
}



C++/CLI project


Header file

Note, that in this C++/CLI example project it is assumed, that DiscountingBondEngine is always used for pricing ZeroCouponBond. Moreover, it is assumed, that pricing engine is always using FlatForward term structure for valuation purposes. Needless to say, these assumptions are used here purely for brevity reasons. It is possible to create class hierarchies in C++/CLI wrapper for instruments, pricing engines and term structures. Such scheme would then enable the usage of more realistic valuation scheme, in which client (C# program) would be able to select desired type for valuation curve and pricing engine for some specific instrument.

Moreover, there is currently only one constructor implementation given for each of the classes. In reality, there are always several ways to create these objects (curve, engine, instrument) in QuantLib.

Finally, there is QuantLibConversion namespace, which offers set of static functions for handling different type conversions between C# types and QuantLib types. Needless to say, there are several ways to implement such type conversion scheme, but (IMHO) implementing TypeConverter would have been a bit overkill for this purpose.

// Wrapper.h
#include "QuantLibLibrary.h"
using namespace System;

namespace Wrapper {

 // class for wrapping native QuantLib::FlatForward
 public ref class QlFlatForward {
 public:
  FlatForward* curve_;

  QlFlatForward(double riskFreeRate, String^ dayCounter, DateTime settlementDate);
  ~QlFlatForward();
  !QlFlatForward();
 };


 // class for wrapping native QuantLib::DiscountingBondEngine
 public ref class QlDiscountingBondEngine {
 public:
  DiscountingBondEngine* pricer_;

  QlDiscountingBondEngine(QlFlatForward^ flatForward);
  ~QlDiscountingBondEngine();
  !QlDiscountingBondEngine();
 };


 // class for wrapping native QuantLib::ZeroCouponBond
 public ref class QlZeroCouponBond {
 public:
  ZeroCouponBond* bond_;

  QlZeroCouponBond(double faceAmount,
   DateTime transactionDate,
   DateTime settlementDate,
   DateTime maturityDate,
   String^ calendar,
   String^ paymentConvention);
  ~QlZeroCouponBond();
  !QlZeroCouponBond();

  double PV();
  void SetPricingEngine(QlDiscountingBondEngine^ engine);

 };


}


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);

 };

}

Implementation file

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

namespace Wrapper {
 using Ql = QuantLibConversions::Convert;


 // implementations for yield curve class
 QlFlatForward::QlFlatForward(double riskFreeRate, String^ dayCounter, DateTime settlementDate) {

  DayCounter dayCounter_ = Ql::ToDayCounter(dayCounter);
  Date settlementDate_ = Ql::ToDate(settlementDate);
  Handle<Quote> riskFreeRateHandle_(boost::make_shared<SimpleQuote>(riskFreeRate));
  curve_ = new FlatForward(settlementDate_, riskFreeRateHandle_, dayCounter_);
 }

 QlFlatForward::~QlFlatForward() {
  this->!QlFlatForward();
 }

 QlFlatForward::!QlFlatForward() {
  delete curve_;
 }



 // implementations for zero bond class
 QlZeroCouponBond::QlZeroCouponBond(double faceAmount,
  DateTime transactionDate,
  DateTime settlementDate,
  DateTime maturityDate,
  String^ calendar,
  String^ paymentConvention) {

  Date transactionDate_ = Ql::ToDate(transactionDate);
  Date settlementDate_ = Ql::ToDate(settlementDate);
  Date maturityDate_ = Ql::ToDate(maturityDate);

  Calendar calendar_ = Ql::ToCalendar(calendar);
  BusinessDayConvention paymentConvention_ = Ql::ToBusinessDayConvention(paymentConvention);
  Natural settlementDays_ = settlementDate_ - transactionDate_;
  bond_ = new ZeroCouponBond(settlementDays_, calendar_, faceAmount, maturityDate_, paymentConvention_);
 }

 QlZeroCouponBond::~QlZeroCouponBond() {
  this->!QlZeroCouponBond();
 }

 QlZeroCouponBond::!QlZeroCouponBond() {
  delete bond_;
 }

 double QlZeroCouponBond::PV() {
  return bond_->NPV();
 }

 void QlZeroCouponBond::SetPricingEngine(QlDiscountingBondEngine^ engine) {
  bond_->setPricingEngine(static_cast<boost::shared_ptr<DiscountingBondEngine>>(engine->pricer_));
 }



 // implementations for zero pricer class
 QlDiscountingBondEngine::QlDiscountingBondEngine(QlFlatForward^ flatForward) {
  
  Handle<YieldTermStructure> discountCurveHandle(static_cast<boost::shared_ptr<FlatForward>>(flatForward->curve_));
  pricer_ = new DiscountingBondEngine(discountCurveHandle);
  Settings::instance().evaluationDate() = flatForward->curve_->referenceDate();
 }

 QlDiscountingBondEngine::~QlDiscountingBondEngine() {
  this->!QlDiscountingBondEngine();
 }

 QlDiscountingBondEngine::!QlDiscountingBondEngine() {
  delete pricer_;
 }


}


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");
 }

}

As always, thanks for reading this blog.
-Mike

No comments:

Post a Comment