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