Wednesday, September 6, 2017

QuantLib : Hull-White one-factor model calibration

Sometimes during the last year I published one post on simulating Hull-White interest rate paths using Quantlib. My conclusion was, that with all the tools provided by this wonderful library, this task should be (relatively) easy thing to do. However, as we know the devil is always in the details - in this case, in the actual process parameters (mean reversion, sigma). Before processing any simulation, we have to get those parameters from somewhere. In this post, we use Quantlib tools for calibrating our model parameters to swaption prices existing in the market.

There are so many variations of this model out there (depending on time-dependencies of different parameters), but the following is the model we are going to use in this example.




Parameters alpha and sigma are constants and theta is time-dependent variable (usually calibrated to current yield curve). Swaption surface is presented in the picture below. Within the table below, time to swaption maturity has been defined in vertical axis, while tenor for underlying swap contract has been defined in horizontal axis. Co-terminal swaptions (used in calibration process) have been specifically marked with yellow colour.























Calibration results


Test cases for three different calibration schemes are included in the example program. More specifically, we :
  1. calibrate the both parameters
  2. calibrate sigma parameter and freeze reversion parameter to 0.05
  3. calibrate reversion parameter and freeze sigma parameter to 0.01
With the given data, we get the following results for these calibration schemes.









The program


As preparatory task, builder class for constructing yield curve should be implemented into a new project from here. After this task, the following header file (ModelCalibrator.h) and tester file (Tester.cpp) should be added to this new project.

First, piecewise yield curve (USD swap curve) and swaption volatilities (co-terminal swaptions) are created by using two free functions in tester. All the data has been hard-coded inside these functions. Needless to say, in any production-level program, this data feed should come from somewhere else. After required market data and ModelCalibrator object has been created, calibration helpers for diagonal swaptions are going to be created and added to ModelCalibrator. Finally, test cases for different calibration schemes are processed.

// ModelCalibrator.h
#pragma once
#include <ql\quantlib.hpp>
#include <algorithm>
//
namespace MJModelCalibratorNamespace
{
 using namespace QuantLib;
 //
 template <typename MODEL, typename OPTIMIZER = LevenbergMarquardt>
 class ModelCalibrator
 {
 public:
  // default implementation (given values) for end criteria class
  ModelCalibrator(const EndCriteria& endCriteria = EndCriteria(1000, 500, 0.0000001, 0.0000001, 0.0000001))
   : endCriteria(endCriteria)
  { }
  void AddCalibrationHelper(boost::shared_ptr<CalibrationHelper>& helper)
  {
   // add any type of calibration helper
   helpers.push_back(helper);
  }
  void Calibrate(boost::shared_ptr<MODEL>& model,
   const boost::shared_ptr<PricingEngine>& pricingEngine,
   const Handle<YieldTermStructure>& curve,
   const std::vector<bool> fixedParameters = std::vector<bool>())
  {
   // assign pricing engine to all calibration helpers
   std::for_each(helpers.begin(), helpers.end(),
    [&pricingEngine](boost::shared_ptr<CalibrationHelper>& helper)
   { helper->setPricingEngine(pricingEngine); });
   //
   // create optimization model for calibrating requested model
   OPTIMIZER solver;
   //
   if (fixedParameters.empty())
   {
    // calibrate all involved parameters
    model->calibrate(helpers, solver, this->endCriteria);
   }
   else
   {
    // calibrate all involved non-fixed parameters
    // hard-coded : vector for weights and constraint type
    model->calibrate(helpers, solver, this->endCriteria, NoConstraint(), std::vector<Real>(), fixedParameters);
   }
  }
 private:
  EndCriteria endCriteria;
  std::vector<boost::shared_ptr<CalibrationHelper>> helpers;
 };
}
//
//
//
//
// Tester.cpp
#include "PiecewiseCurveBuilder.cpp"
#include "ModelCalibrator.h"
#include <iostream>
//
using namespace MJPiecewiseCurveBuilderNamespace;
namespace MJ_Calibrator = MJModelCalibratorNamespace;
//
// declarations for two free functions used to construct required market data
void CreateCoTerminalSwaptions(std::vector<Volatility>& diagonal);
void CreateYieldCurve(RelinkableHandle<YieldTermStructure>& curve, 
 const Date& settlementDate, const Calendar& calendar);
//
//
//
int main()
{
 try
 {
  // dates
  Date tradeDate(4, September, 2017);
  Settings::instance().evaluationDate() = tradeDate;
  Calendar calendar = TARGET();
  Date settlementDate = calendar.advance(tradeDate, Period(2, Days), ModifiedFollowing);
  //
  // market data : create piecewise yield curve
  RelinkableHandle<YieldTermStructure> curve;
  CreateYieldCurve(curve, settlementDate, calendar);
  //
  // market data : create co-terminal swaption volatilities
  std::vector<Volatility> diagonal;
  CreateCoTerminalSwaptions(diagonal);
  //
  // create model calibrator
  MJ_Calibrator::ModelCalibrator<HullWhite> modelCalibrator;
  //
  // create and add calibration helpers to model calibrator
  boost::shared_ptr<IborIndex> floatingIndex(new USDLibor(Period(3, Months), curve));
  for (unsigned int i = 0; i != diagonal.size(); ++i)
  {
   int timeToMaturity = i + 1;
   int underlyingTenor = diagonal.size() - i;
   //
   // using 1st constructor for swaption helper class
   modelCalibrator.AddCalibrationHelper(boost::shared_ptr<CalibrationHelper>(new SwaptionHelper(
     Period(timeToMaturity, Years), // time to swaption maturity
     Period(underlyingTenor, Years), // tenor of the underlying swap
     Handle<Quote>(boost::shared_ptr<Quote>(new SimpleQuote(diagonal[i]))), // swaption volatility
     floatingIndex, // underlying floating index
     Period(1, Years), // tenor for underlying fixed leg
     Actual360(), // day counter for underlying fixed leg
     floatingIndex->dayCounter(), // day counter for underlying floating leg
     curve))); // term structure
  }
  //
  // create model and pricing engine, calibrate model and print calibrated parameters
  // case 1 : calibrate all involved parameters (HW1F : reversion, sigma)
  boost::shared_ptr<HullWhite> model(new HullWhite(curve));
  boost::shared_ptr<PricingEngine> jamshidian(new JamshidianSwaptionEngine(model));
  modelCalibrator.Calibrate(model, jamshidian, curve);
  std::cout << "calibrated reversion = " << model->params()[0] << std::endl;
  std::cout << "calibrated sigma = " << model->params()[1] << std::endl;
  std::cout << std::endl;
  //
  // case 2 : calibrate sigma and fix reversion to famous 0.05
  model = boost::shared_ptr<HullWhite>(new HullWhite(curve, 0.05, 0.0001));
  jamshidian = boost::shared_ptr<PricingEngine>(new JamshidianSwaptionEngine(model));
  std::vector<bool> fixedReversion = { true, false };
  modelCalibrator.Calibrate(model, jamshidian, curve, fixedReversion);
  std::cout << "fixed reversion = " << model->params()[0] << std::endl;
  std::cout << "calibrated sigma = " << model->params()[1] << std::endl;
  std::cout << std::endl;
  //
  // case 3 : calibrate reversion and fix sigma to 0.01
  model = boost::shared_ptr<HullWhite>(new HullWhite(curve, 0.05, 0.01));
  jamshidian = boost::shared_ptr<PricingEngine>(new JamshidianSwaptionEngine(model));
  std::vector<bool> fixedSigma = { false, true };
  modelCalibrator.Calibrate(model, jamshidian, curve, fixedSigma);
  std::cout << "calibrated reversion = " << model->params()[0] << std::endl;
  std::cout << "fixed sigma = " << model->params()[1] << std::endl;
 }
 catch (std::exception& e)
 {
  std::cout << e.what() << std::endl;
 }
 return 0;
}
//
void CreateCoTerminalSwaptions(std::vector<Volatility>& diagonal)
{
 // hard-coded data
 // create co-terminal swaptions 
 diagonal.push_back(0.3133); // 1x10
 diagonal.push_back(0.3209); // 2x9
 diagonal.push_back(0.3326); // 3x8
 diagonal.push_back(0.331); // 4x7
 diagonal.push_back(0.3281); // 5x6
 diagonal.push_back(0.318); // 6x5
 diagonal.push_back(0.3168); // 7x4
 diagonal.push_back(0.3053); // 8x3
 diagonal.push_back(0.2992); // 9x2
 diagonal.push_back(0.3073); // 10x1
}
//
void CreateYieldCurve(RelinkableHandle<YieldTermStructure>& curve,
 const Date& settlementDate, const Calendar& calendar) 
{
 // hard-coded data
 // create piecewise yield curve by using builder class
 DayCounter curveDaycounter = Actual360();
 PiecewiseCurveBuilder<ZeroYield, Linear> builder;
 //
 // cash rates
 pQuote q_1W(new SimpleQuote(0.012832));
 pIndex i_1W(new USDLibor(Period(1, Weeks)));
 builder.AddDeposit(q_1W, i_1W);
 //
 pQuote q_1M(new SimpleQuote(0.012907));
 pIndex i_1M(new USDLibor(Period(1, Months)));
 builder.AddDeposit(q_1M, i_1M);
 //
 pQuote q_3M(new SimpleQuote(0.0131611));
 pIndex i_3M(new USDLibor(Period(3, Months))); 
 builder.AddDeposit(q_3M, i_3M);
 //
 // futures
 Date IMMDate;
 pQuote q_DEC17(new SimpleQuote(98.5825));
 IMMDate = IMM::nextDate(settlementDate + Period(3, Months));
 builder.AddFuture(q_DEC17, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 pQuote q_MAR18(new SimpleQuote(98.5425));
 IMMDate = IMM::nextDate(settlementDate + Period(6, Months));
 builder.AddFuture(q_MAR18, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 pQuote q_JUN18(new SimpleQuote(98.4975));
 IMMDate = IMM::nextDate(settlementDate + Period(9, Months));
 builder.AddFuture(q_JUN18, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 pQuote q_SEP18(new SimpleQuote(98.4475));
 IMMDate = IMM::nextDate(settlementDate + Period(12, Months));
 builder.AddFuture(q_SEP18, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 pQuote q_DEC18(new SimpleQuote(98.375));
 IMMDate = IMM::nextDate(settlementDate + Period(15, Months));
 builder.AddFuture(q_DEC18, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 pQuote q_MAR19(new SimpleQuote(98.3425));
 IMMDate = IMM::nextDate(settlementDate + Period(18, Months));
 builder.AddFuture(q_MAR19, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 pQuote q_JUN19(new SimpleQuote(98.3025));
 IMMDate = IMM::nextDate(settlementDate + Period(21, Months));
 builder.AddFuture(q_JUN19, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 pQuote q_SEP19(new SimpleQuote(98.2675));
 IMMDate = IMM::nextDate(settlementDate + Period(24, Months));
 builder.AddFuture(q_SEP19, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 pQuote q_DEC19(new SimpleQuote(98.2125));
 IMMDate = IMM::nextDate(settlementDate + Period(27, Months));
 builder.AddFuture(q_DEC19, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 pQuote q_MAR20(new SimpleQuote(98.1775));
 IMMDate = IMM::nextDate(settlementDate + Period(30, Months));
 builder.AddFuture(q_MAR20, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 pQuote q_JUN20(new SimpleQuote(98.1425));
 IMMDate = IMM::nextDate(settlementDate + Period(33, Months));
 builder.AddFuture(q_JUN20, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 // swaps
 pIndex swapFloatIndex(new USDLibor(Period(3, Months))); 
 pQuote q_4Y(new SimpleQuote(0.01706));
 builder.AddSwap(q_4Y, Period(4, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_5Y(new SimpleQuote(0.0176325));
 builder.AddSwap(q_5Y, Period(5, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_6Y(new SimpleQuote(0.01874));
 builder.AddSwap(q_6Y, Period(6, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_7Y(new SimpleQuote(0.0190935));
 builder.AddSwap(q_7Y, Period(7, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_8Y(new SimpleQuote(0.02011));
 builder.AddSwap(q_8Y, Period(8, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_9Y(new SimpleQuote(0.02066));
 builder.AddSwap(q_9Y, Period(9, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_10Y(new SimpleQuote(0.020831));
 builder.AddSwap(q_10Y, Period(10, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_11Y(new SimpleQuote(0.02162));
 builder.AddSwap(q_11Y, Period(11, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_12Y(new SimpleQuote(0.0217435));
 builder.AddSwap(q_12Y, Period(12, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_15Y(new SimpleQuote(0.022659));
 builder.AddSwap(q_15Y, Period(15, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_20Y(new SimpleQuote(0.0238125));
 builder.AddSwap(q_20Y, Period(20, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_25Y(new SimpleQuote(0.0239385));
 builder.AddSwap(q_25Y, Period(25, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_30Y(new SimpleQuote(0.02435));
 builder.AddSwap(q_30Y, Period(30, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 curve = builder.GetCurveHandle(settlementDate, curveDaycounter);
}


I have noticed, that there are some rule-of-thumbs, but the actual calibration for any of these models is not so straightforward as one may think. There are some subtle issues on market data quality and products under pricing, which are leaving us with relatively high degree of freedom. Further step towards the calibration red pill can be taken by checking out this excellent research paper, written by the guys from Mizuho Securities.

Finally, as always, thanks a lot again for reading this blog.
-Mike

Sunday, September 3, 2017

QuantLib : another implementation for piecewise yield curve builder class


Last year I published one possible implementation using Quantlib library for constructing piecewise yield curves. Within this second implementation, I have done a couple of changes in order to increase configurability. For allowing more flexible curve construction algorithm, Traits and Interpolations are now defined as template parameters. Previously, these were hard-coded inside the class. Secondly, all types of quotes for class methods are now wrapped inside shared pointers. Previously, there was an option to give quotes as rates. After some reconsideration, I have decided to give up this option completely. Finally, I have added a class method for allowing futures prices to be used in curve construction process.


Source data and results


The following source data from the book by Richard Flavell has been used for validation.



















Resulting zero-yield curve and discount factors are presented in the graph below. For this data used, the absolute maximum difference between Flavell-constructed and Quantlib-constructed zero yield curves is around one basis point.

















The program


// PiecewiseCurveBuilder.h
#pragma once
#include <ql/quantlib.hpp>
//
namespace MJPiecewiseCurveBuilderNamespace
{
 using namespace QuantLib;
 //
 // type alias definitions
 using pQuote = boost::shared_ptr<Quote>;
 using pIndex = boost::shared_ptr<IborIndex>;
 using pHelper = boost::shared_ptr<RateHelper>;
 //
 // T = traits, I = interpolation
 template<typename T, typename I>
 class PiecewiseCurveBuilder
 {
 public:
  PiecewiseCurveBuilder();
  void AddDeposit(const pQuote& quote, const pIndex& index);
  void AddFRA(const pQuote& quote, const Period& periodLengthToStart, const pIndex& index);
  void AddSwap(const pQuote& quote, const Period& periodLength, const Calendar& fixedCalendar, Frequency fixedFrequency,
   BusinessDayConvention fixedConvention, const DayCounter& fixedDayCount, const pIndex& floatIndex);
  //
  void AddFuture(const pQuote& quote, const Date& IMMDate, int lengthInMonths, const Calendar& calendar,
   BusinessDayConvention convention, const DayCounter& dayCounter, bool endOfMonth = true);
  //
  RelinkableHandle<YieldTermStructure> GetCurveHandle(const Date& settlementDate, const DayCounter& dayCounter);
 private:
  std::vector<pHelper> rateHelpers;

 };
}
//
//
//
//
// PiecewiseCurveBuilder.cpp
#pragma once
#include "PiecewiseCurveBuilder.h"
//
namespace MJPiecewiseCurveBuilderNamespace
{
 template<typename T, typename I>
 PiecewiseCurveBuilder<T, I>::PiecewiseCurveBuilder() { }
 //
 template<typename T, typename I>
 void PiecewiseCurveBuilder<T, I>::AddDeposit(const pQuote& quote, const pIndex& index)
 {
  pHelper rateHelper(new DepositRateHelper(Handle<Quote>(quote), index));
  rateHelpers.push_back(rateHelper);
 }
 //
 template<typename T, typename I>
 void PiecewiseCurveBuilder<T, I>::AddFRA(const pQuote& quote, const Period& periodLengthToStart, const pIndex& index)
 {
  pHelper rateHelper(new FraRateHelper(Handle<Quote>(quote),
   periodLengthToStart, pIndex(index)));
  rateHelpers.push_back(rateHelper);
 }
 //
 template<typename T, typename I>
 void PiecewiseCurveBuilder<T, I>::AddSwap(const pQuote& quote, const Period& periodLength, const Calendar& fixedCalendar,
  Frequency fixedFrequency, BusinessDayConvention fixedConvention, const DayCounter& fixedDayCount,
  const pIndex& floatIndex)
 {
  pHelper rateHelper(new SwapRateHelper(Handle<Quote>(quote), periodLength, fixedCalendar, fixedFrequency,
   fixedConvention, fixedDayCount, floatIndex));
  rateHelpers.push_back(rateHelper);
 }
 //
 template<typename T, typename I>
 void PiecewiseCurveBuilder<T, I>::AddFuture(const pQuote& quote, const Date& IMMDate, int lengthInMonths, const Calendar& calendar,
  BusinessDayConvention convention, const DayCounter& dayCounter, bool endOfMonth)
 {
  pHelper rateHelper(new FuturesRateHelper(Handle<Quote>(quote), IMMDate, lengthInMonths, calendar, convention, endOfMonth, dayCounter));
  rateHelpers.push_back(rateHelper);
 }
 //
 template<typename T, typename I>
 RelinkableHandle<YieldTermStructure> PiecewiseCurveBuilder<T, I>::GetCurveHandle(const Date& settlementDate, const DayCounter& dayCounter)
 {
  // T = traits, I = interpolation
  boost::shared_ptr<YieldTermStructure> yieldTermStructure(new PiecewiseYieldCurve<T, I>(settlementDate, rateHelpers, dayCounter));
  return RelinkableHandle<YieldTermStructure>(yieldTermStructure);
 }
}
//
//
//
//
// Tester.cpp
#include "PiecewiseCurveBuilder.cpp"
#include <iostream>
using namespace MJPiecewiseCurveBuilderNamespace;
//
int main()
{
 try
 {
  Date tradeDate(4, February, 2008);
  Settings::instance().evaluationDate() = tradeDate;
  Calendar calendar = TARGET();
  Date settlementDate = calendar.advance(tradeDate, Period(2, Days), ModifiedFollowing);
  DayCounter curveDaycounter = Actual360();
  PiecewiseCurveBuilder<ZeroYield, Linear> builder;
  //
  //
  // cash part of the curve
  pQuote q_1W(new SimpleQuote(0.032175));
  pIndex i_1W(new USDLibor(Period(1, Weeks)));
  builder.AddDeposit(q_1W, i_1W);
  //
  pQuote q_1M(new SimpleQuote(0.0318125));
  pIndex i_1M(new USDLibor(Period(1, Months)));
  builder.AddDeposit(q_1M, i_1M);
  //
  pQuote q_3M(new SimpleQuote(0.03145));
  pIndex i_3M(new USDLibor(Period(3, Months)));
  builder.AddDeposit(q_3M, i_3M);
  //
  //
  // futures part of the curve
  Date IMMDate;
  pQuote q_JUN08(new SimpleQuote(97.41));
  IMMDate = IMM::nextDate(settlementDate + Period(4, Months));
  builder.AddFuture(q_JUN08, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
  //
  pQuote q_SEP08(new SimpleQuote(97.52));
  IMMDate = IMM::nextDate(settlementDate + Period(7, Months));
  builder.AddFuture(q_SEP08, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
  //
  pQuote q_DEC08(new SimpleQuote(97.495));
  IMMDate = IMM::nextDate(settlementDate + Period(10, Months));
  builder.AddFuture(q_DEC08, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
  //
  pQuote q_MAR09(new SimpleQuote(97.395));
  IMMDate = IMM::nextDate(settlementDate + Period(13, Months));
  builder.AddFuture(q_MAR09, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
  //
  //
  // swap part of the curve
  pIndex swapFloatIndex(new USDLibor(Period(3, Months)));
  pQuote q_2Y(new SimpleQuote(0.02795));
  builder.AddSwap(q_2Y, Period(2, Years), calendar, Annual,
   ModifiedFollowing, Actual360(), swapFloatIndex);
  //
  pQuote q_3Y(new SimpleQuote(0.03035));
  builder.AddSwap(q_3Y, Period(3, Years), calendar, Annual,
   ModifiedFollowing, Actual360(), swapFloatIndex);
  //
  pQuote q_4Y(new SimpleQuote(0.03275));
  builder.AddSwap(q_4Y, Period(4, Years), calendar, Annual,
   ModifiedFollowing, Actual360(), swapFloatIndex);
  //
  pQuote q_5Y(new SimpleQuote(0.03505));
  builder.AddSwap(q_5Y, Period(5, Years), calendar, Annual,
   ModifiedFollowing, Actual360(), swapFloatIndex);
  //
  pQuote q_6Y(new SimpleQuote(0.03715));
  builder.AddSwap(q_6Y, Period(6, Years), calendar, Annual,
   ModifiedFollowing, Actual360(), swapFloatIndex);
  //
  pQuote q_7Y(new SimpleQuote(0.03885));
  builder.AddSwap(q_7Y, Period(7, Years), calendar, Annual,
   ModifiedFollowing, Actual360(), swapFloatIndex);
  //
  pQuote q_8Y(new SimpleQuote(0.04025));
  builder.AddSwap(q_8Y, Period(8, Years), calendar, Annual,
   ModifiedFollowing, Actual360(), swapFloatIndex);
  //
  pQuote q_9Y(new SimpleQuote(0.04155));
  builder.AddSwap(q_9Y, Period(9, Years), calendar, Annual,
   ModifiedFollowing, Actual360(), swapFloatIndex);
  //
  pQuote q_10Y(new SimpleQuote(0.04265));
  builder.AddSwap(q_10Y, Period(10, Years), calendar, Annual,
   ModifiedFollowing, Actual360(), swapFloatIndex);
  //
  pQuote q_12Y(new SimpleQuote(0.04435));
  builder.AddSwap(q_12Y, Period(12, Years), calendar, Annual,
   ModifiedFollowing, Actual360(), swapFloatIndex);
  //
  //
  // get curve handle and print out discount factors
  RelinkableHandle<YieldTermStructure> curve = builder.GetCurveHandle(settlementDate, curveDaycounter);
  std::cout << curve->discount(Date(11, February, 2008)) << std::endl;
  std::cout << curve->discount(Date(4, March, 2008)) << std::endl;
  std::cout << curve->discount(Date(4, May, 2008)) << std::endl;
  std::cout << curve->discount(Date(6, August, 2008)) << std::endl;
  std::cout << curve->discount(Date(6, November, 2008)) << std::endl;
  std::cout << curve->discount(Date(6, February, 2009)) << std::endl;
  std::cout << curve->discount(Date(8, February, 2010)) << std::endl;
  std::cout << curve->discount(Date(7, February, 2011)) << std::endl;
  std::cout << curve->discount(Date(6, February, 2012)) << std::endl;
  std::cout << curve->discount(Date(6, February, 2013)) << std::endl;
  std::cout << curve->discount(Date(6, February, 2014)) << std::endl;
  std::cout << curve->discount(Date(6, February, 2015)) << std::endl;
  std::cout << curve->discount(Date(8, February, 2016)) << std::endl;
  std::cout << curve->discount(Date(6, February, 2017)) << std::endl;
  std::cout << curve->discount(Date(6, February, 2018)) << std::endl;
 }
 catch (std::exception& e)
 {
  std::cout << e.what() << std::endl;
 }
 return 0;
}


Data updating


Since all rates have been wrapped inside quotes (which have been wrapped inside handles), those can be accessed only by using dynamic pointer casting. Below is an example for updating 3M cash quote.

boost::dynamic_pointer_cast<SimpleQuote>(q_3M)->setValue(0.03395);

After this quote updating shown above, a new requested value (zero rate, forward rate or discount factor) from constructed curve object will be re-calculated by using updated quote.

For those readers who are completely unfamiliar with this stuff presented, there are three extremely well-written slides available in Quantlib documentation page, written by Dimitri Reiswich. These slides are offering excellent way to get very practical hands-on overview on QuantLib library. Also, Luigi Ballabio has finally been finishing his book on QuantLib implementation, which can be purchased from Leanpub. This book is offering deep diving experience into the abyss of Quantlib architechture.

Finally, as usual, thanks for spending your precious time here and reading this blog.
-Mike

Sunday, August 6, 2017

C++11 : modelling one-factor processes using functional programming paradigm


There was an interesting technical article on July 2017 Wilmott magazine written by Daniel Duffy and Avi Palley. This multi-page article was giving an overview on some "game changing" Boost libraries, which have been accepted as a part of C++11/14 standard, such as smart pointers, function wrappers, lambda expressions and tuples. The second part of this article series will be published in the next Wilmott magazine and it will present (according to editor) design and implementation for Monte Carlo option pricing framework. It is also an important point of mentioning, that (according to Amazon.com) Daniel Duffy is about to publish long-awaited second edition of his book on pricing derivatives using C++. The book was initially published somewhere back in 2004 and the landscape has changed quite dramatically since these days.

Within the last chapter of this article, functional programming paradigm was nicely applied for modelling one-factor stochastic differential equations, generally used in Finance. By applying more functional programming paradigm, the usually observed code bloating can be substantially reduced. As an example of such code bloat, I reviewed my own implementation for path generator, which models one-factor processes for Monte Carlo purposes. There is an abstract base class (OneFactorProcess) and implementations for GBM and Vasicek processes. Even there is nothing fundamentally wrong with this approach (class hierarchy), one may ask, whether there would be a bit more flexible ways to implement this kind of a scheme.

Within this post, I have been re-designing modelling part for one-factor processes by applying functional programming paradigm, as presented in that article. Reduction in code bloating is present, since there is currently only one single class for modelling different types of processes (before, there was a class hierarchy). Moreover, since the both functions for handling drift and diffusion terms will be constructed outside of this class, their construction process is now much more flexible than before.


The program

 

Implement the following program (two header files and one implementation file for tester) into a new project. For brevity reasons, I have re-designed only the part of the program, which models one-factor processes. Monte Carlo part has been implemented as free function in tester implementation file. First, one-factor process object (Process) will be created by using ProcessBuilder object (Builder Pattern). Within this example, I have implemented a builder for constructing Process object by using console. However, the flexibility in this program allows different types of builders to be implemented. As soon as Process object is created, "skeleton path" (std::vector) will be sent to Monte Carlo method (MonteCarloLite), along with all simulation-related attributes and Process object. As a result, this method will fill vector with the values from one simulation path for chosen parameters and applied stochastic process. Finally, a path will be printed back to console.

#pragma once
#include <functional>
// OneFactorSDE.h
namespace FunctionalOneFactorProcessExampleNamespace
{
 // blueprint for a function, which models drift or diffusion
 using Function = std::function<double(double s, double t)>;
 // wrapper for drift and diffusion function components
 using Functions = std::tuple<Function, Function>;
 //
 // class for modeling one-factor processes by employing drift and diffusion functions
 class Process
 {
 public:
  Process(Functions functions, double initialCondition)
   : driftFunction(std::get<0>(functions)), diffusionFunction(std::get<1>(functions)),
   initialCondition(initialCondition) { }
  double drift(double s, double t) { return this->driftFunction(s, t); }
  double diffusion(double s, double t) { return this->diffusionFunction(s, t); }
  double InitialCondition() const { return this->initialCondition; }
  //
 private:
  double initialCondition; // spot price for a process
  Function driftFunction; // function for modelling process drift
  Function diffusionFunction;  // function for modelling process diffusion
 };
}
//
//
//
// ProcessFactory.h
#pragma once
#include <iostream>
#include <string>
#include <memory>
#include "OneFactorSDE.h"
//
namespace FunctionalOneFactorProcessExampleNamespace
{
 // abstract base class for all process builders
 class ProcessBuilder
 {
 public:
  // return process object which is wrapped inside shared pointer
  // let implementation classes decide, how and from where to built process object
  virtual std::shared_ptr<Process> Build() = 0;
 };
 //
 // specific implementation for console process builder
 // process type and corresponding parameters will be requested from a client by using console
 class ConsoleProcessBuilder : public ProcessBuilder
 {
 public:
  std::shared_ptr<Process> Build() override
  {
   Functions functions;
   double initialCondition = 0.0;
   std::string consoleSelection;
   std::cout << "Select process [1 = GBM, 2 = Vasicek] > ";
   // if conversion cannot be performed, stoi will throw invalid argument exception
   std::getline(std::cin, consoleSelection);
   int processID = std::stoi(consoleSelection);
   //
   switch (processID)
   {
   // GBM process
   case 1:
   {
    // receive client inputs
    std::cout << "spot price > ";
    std::getline(std::cin, consoleSelection);
    initialCondition = std::stod(consoleSelection);
    //
    std::cout << "risk-free rate > ";
    std::getline(std::cin, consoleSelection);
    double r = std::stod(consoleSelection);
    //
    std::cout << "volatility > ";
    std::getline(std::cin, consoleSelection);
    double v = std::stod(consoleSelection);
    //
    // build drift and diffusion functions for GBM process object
    auto driftFunction = [r](double S, double T){ return r * S; };
    auto diffusionFunction = [v](double S, double T){ return v * S; };
    // wrap drift and diffusion functions into tuple
    functions = std::make_tuple(driftFunction, diffusionFunction);
    break;
   }
   case 2:
   {
    // receive client inputs
    std::cout << "spot price > ";
    std::getline(std::cin, consoleSelection);
    initialCondition = std::stod(consoleSelection);
    //
    std::cout << "reversion > ";
    std::getline(std::cin, consoleSelection);
    double reversion = std::stod(consoleSelection);
    //
    std::cout << "long-term rate > ";
    std::getline(std::cin, consoleSelection);
    double longTermRate = std::stod(consoleSelection);
    //
    std::cout << "rate volatility > ";
    std::getline(std::cin, consoleSelection);
    double v = std::stod(consoleSelection);
    //
    // build drift and diffusion functions for Vasicek process object
    auto driftFunction = [reversion, longTermRate](double S, double T)
     { return reversion * (longTermRate - S); };
    auto diffusionFunction = [v](double S, double T){ return v; };
    // wrap drift and diffusion functions into tuple
    functions = std::make_tuple(driftFunction, diffusionFunction);
    break;
   }
   default:
    // if selected process is not configured, program will throw invalid argument exception
    throw std::invalid_argument("invalid process ID");
    break;
   }
   // build and return constructed process object for a client
   // wrapped into shared pointer
   return std::shared_ptr<Process>(new Process(functions, initialCondition));
  }
 };
}
//
//
//
// Tester.cpp
#include <vector>
#include <random>
#include <algorithm>
#include <chrono>
#include "ProcessFactory.h"
namespace MJ = FunctionalOneFactorProcessExampleNamespace;
//
void MonteCarloLite(std::vector<double>& path, 
 std::shared_ptr<MJ::Process>& process, const double maturity);
//
int main()
{
 try
 {
  // create process object by using console builder
  MJ::ConsoleProcessBuilder builder;
  std::shared_ptr<MJ::Process> process = builder.Build();
  //
  // create simulation-related attributes
  int nSteps = 100;
  double timeToMaturity = 1.25;
  // create, process and print one path
  std::vector<double> path(nSteps);
  MonteCarloLite(path, process, timeToMaturity);
  std::for_each(path.begin(), path.end(), 
   [](double v) { std::cout << v << std::endl; });
 }
 catch (std::exception e)
 {
  std::cout << e.what() << std::endl;
 }
 return 0;
}
//
void MonteCarloLite(std::vector<double>& path, 
 std::shared_ptr<MJ::Process>& process, const double maturity)
{
 // lambda method for seeding uniform random generator
 std::function<unsigned long(void)> seeder =
  [](void) -> unsigned long { return static_cast<unsigned long>
  (std::chrono::steady_clock::now().time_since_epoch().count()); };
 //
 // create uniform generator, distribution and random generator function
 std::mt19937 uniformGenerator(seeder());
 std::normal_distribution<double> distribution;
 std::function<double(double)> randomGenerator;
 //
 // lambda method for processing standard normal random numbers
 randomGenerator = [uniformGenerator, distribution](double x) mutable -> double
 {
  x = distribution(uniformGenerator);
  return x;
 };
 //
 // create vector of standard normal random numbers
 // use lambda method created earlier
 std::transform(path.begin(), path.end(), path.begin(), randomGenerator);
 //
 double dt = maturity / (path.size() - 1);
 double dw = 0.0;
 double s = (*process).InitialCondition();
 double t = 0.0;
 path[0] = s; // 1st path element is always the current spot price
 //
 // transform random number vector into a path containing asset prices
 for (auto it = path.begin() + 1; it != path.end(); ++it)
 {
  t += dt;
  dw = (*it) * std::sqrt(dt);
  (*it) = s + (*process).drift(s, t) * dt + (*process).diffusion(s, t) * dw;
  s = (*it);
 }
}


As always, thanks a lot for reading this blog.
-Mike

Wednesday, July 26, 2017

AlgLib : Ho-Lee Calibration Using Levenberg-Marquardt algorithm in VBA

Some time ago, I published one possible C# implementation for Ho-Lee one-factor model calibration scheme using AlgLib numerical libraries. This time I will present an implementation for the same scheme in VBA. General information concerning Levenberg-Marquardt algorithm implementation in AlgLib has been presented in here. Libraries for VBA (collection of BAS module files, which are going to be included into VBA project) can be downloaded from here.

A few words about this implementation. AlgLibLMASolver class uses AlgLib library functions (functions from 21 different modules) for processing (creating optimization model, setting conditions, processing iterations). One data member within this class is having a type of IModel. This data member is actually a reference to an interface, which provides a set of functions for all required calculations (objective function value, values for function terms, partial derivative values for function terms). Since all possible implementations for any interface method must honor signatures exactly, there is a problem with VBA since it does not have a real constructor mechanism. I have chewed this issue in here. It might help to explain the reason, why I have been distributing input parameters for interface implementation by using Dictionary object. Finally, HoLeeZeroCouponCalibration class is implementing IModel interface (a set of functions for all required calculations). In essence, algorithms (AlgLib-related processing) and data (values calculated specifically by using Ho-Lee model) have been completely separated. Needless to say, this type of scheme is flexible for new implementations.

Create a new VBA project and copy-paste the following classes and modules into this project. Also, import all required 21 AlgLib BAS files into this project.

' CLASS : AlgLibLMASolver
Option Explicit
'
' The following 21 AlgLib modules are required for succesfull compilation of this project :
' ablas, ablasf, ap, bdsvd, blas, creflections, densesolver, hblas, linmin, matinv,
' minlbfgs, minlm, ortfac, rcond, reflections, rotations, safesolve, sblas, svd, trfac, xblas
'
Private state As MinLMState
Private report As MinLMReport
Private n As Long
Private m As Long
Private x() As Double
Private model As IModel
Private epsF As Double
Private epsG As Double
Private epsX As Double
Private iterations As Long
'
Public Function initialize( _
    ByVal numberOfVariables As Long, _
    ByVal numberOfEquations As Long, _
    ByRef changingVariables() As Double, _
    ByRef callbackModel As IModel, _
    ByVal epsilonF As Double, _
    ByVal epsilonG As Double, _
    ByVal epsilonX As Double, _
    ByVal maximumIterations As Long)
    '
    n = numberOfVariables
    m = numberOfEquations
    x = changingVariables
    Set model = callbackModel
    epsF = epsilonF
    epsG = epsilonG
    epsX = epsilonX
    iterations = maximumIterations
End Function
'
Public Sub Solve()
    '
    ' create solver scheme using functions and analytical partial derivatives
    Call MinLMCreateFJ(n, m, x, state)
    ' set stopping conditions
    Call MinLMSetCond(state, epsG, epsF, epsX, iterations)
    '
    ' process iterations
    Do While MinLMIteration(state)
        '
        ' calculate value for objective function
        If (state.NeedF) Then
            '
            model.callBackObjectiveFunction state
        End If
        '
        ' calculate values for functions and partial derivatives
        If (state.NeedFiJ) Then
            '
            model.callBackFunction state
            model.callBackJacobian state
        End If
    Loop
    '
    ' process results
    Call MinLMResults(state, x, report)
End Sub
'
' public accessor to (MinLMState) state
Public Property Get GetState() As MinLMState
    GetState = state
End Property
'
' public accessor to (MinLMReport) report
Public Property Get GetReport() As MinLMReport
    GetReport = report
End Property
'
' public accessor to hard-coded report
Public Property Get GetPrettyPrintReport() As String
    '
    Dim message As String
    message = "*** AlgLibLMASolver execution report " + VBA.CStr(VBA.Now) + " ***" + VBA.vbNewLine
    message = message + "TerminationType : " + VBA.CStr(report.TerminationType) + VBA.vbNewLine
    message = message + "Iterations : " + VBA.CStr(report.IterationsCount) + VBA.vbNewLine
    message = message + "Objective function : " + VBA.CStr(state.f) + VBA.vbNewLine
    message = message + VBA.vbNewLine
    '
    Dim i As Integer
    For i = 0 To (state.n - 1)
        message = message + "x(" + VBA.CStr(i) + ") = " + VBA.CStr(state.x(i)) + VBA.vbNewLine
    Next i
    '
    GetPrettyPrintReport = message
End Property
'
'
'
'
'
' CLASS : IModel
Option Explicit
' set of functions for IModel interface
Public Function initialize(ByRef parameters As Scripting.Dictionary)
    ' assign required member data wrapped into dictionary
End Function
'
Public Function callBackObjectiveFunction(ByRef state As MinLMState)
    ' calculate objective function value
End Function
'
Public Function callBackFunction(ByRef state As MinLMState)
    ' calculate values for (non-squared) function terms
End Function
'
Public Function callBackJacobian(ByRef state As MinLMState)
    ' calculate partial derivative values for (non-squared) function terms
End Function
'
'
'
'
'
' CLASS : HoLeeZeroCouponCalibration
Option Explicit
'
Implements IModel
'
Private s As Double
Private r As Double
Private t() As Double
Private z() As Double
'
Private Function IModel_initialize(ByRef parameters As Scripting.IDictionary)
    '
    s = parameters(HOLEE_PARAMETERS.sigma)
    r = parameters(HOLEE_PARAMETERS.shortRate)
    t = parameters(HOLEE_PARAMETERS.maturity)
    z = parameters(HOLEE_PARAMETERS.zeroCouponBond)
End Function
'
Private Function IModel_callBackObjectiveFunction(ByRef state As MinLMState)
    '
    ' calculate value for aggregate objective function
    Dim i As Integer
    Dim hoLeeZero As Double
    Dim f As Double: f = 0
    '
    ' loop through number of equations
    For i = 0 To (state.m - 1)
        '
        hoLeeZero = VBA.Exp(-(1 / 2) * state.x(i) * (t(i) ^ 2) + (1 / 6) * (s ^ 2) * (t(i) ^ 3) - r * t(i))
        f = f + (z(i) - hoLeeZero) ^ 2
    Next i
    state.f = f
End Function
'
Private Function IModel_callBackFunction(ByRef state As MinLMState)
    '
    ' calculate values for (non-squared) function terms
    Dim i As Integer
    Dim hoLeeZero As Double
    '
    ' loop through number of equations
    For i = 0 To (state.m - 1)
        '
        hoLeeZero = VBA.Exp(-(1 / 2) * state.x(i) * (t(i) ^ 2) + (1 / 6) * (s ^ 2) * (t(i) ^ 3) - r * t(i))
        state.FI(i) = (z(i) - hoLeeZero)
    Next i
End Function
'
Private Function IModel_callBackJacobian(ByRef state As MinLMState)
    '
    ' calculate partial derivative values for (non-squared) function terms
    Dim i As Integer, J As Integer
    Dim hoLeeZero As Double
    '
    ' 1. individual (non-squared) function terms
    ' loop through number of equations
    For i = 0 To (state.m - 1)
        '
        hoLeeZero = VBA.Exp(-(1 / 2) * state.x(i) * (t(i) ^ 2) + (1 / 6) * (s ^ 2) * (t(i) ^ 3) - r * t(i))
        state.FI(i) = (z(i) - hoLeeZero)
    Next i
    '
    ' 2. partial derivatives for all (non-squared) function terms
    ' loop through number of equations
    For i = 0 To (state.m - 1)
        '
    ' loop through number of variables
        For J = 0 To (state.n - 1)
            '
            Dim derivative As Double: derivative = 0
            ' partial derivative is non-zero only for diagonal cases
            If (i = J) Then
                derivative = (1 / 2) * VBA.Exp(1) * t(J) ^ 2
                state.J(i, J) = derivative
            End If
        Next J
    Next i
End Function
'
'
'
'
'
' MODULE : DataStructures
Option Explicit
'
Public Enum HOLEE_PARAMETERS
    sigma = 1
    shortRate = 2
    maturity = 3
    zeroCouponBond = 4
End Enum
'
'
'
'
'
' TESTER MODULE
Option Explicit
'
' Ho-Lee model calibration example
Public Sub AlglibTester()
    '
    ' MODEL part
    ' construct all required inputs and model to be calibrated
    Dim sigma As Double: sigma = 0.00039
    Dim shortRate As Double: shortRate = 0.00154
    '
    Dim maturity(0 To 9) As Double
    maturity(0) = 1: maturity(1) = 2: maturity(2) = 3: maturity(3) = 4: maturity(4) = 5:
    maturity(5) = 6: maturity(6) = 7: maturity(7) = 8: maturity(8) = 9: maturity(9) = 10
    '
    Dim zero(0 To 9) As Double
    zero(0) = 0.9964: zero(1) = 0.9838: zero(2) = 0.9611: zero(3) = 0.9344: zero(4) = 0.9059:
    zero(5) = 0.8769: zero(6) = 0.8478: zero(7) = 0.8189: zero(8) = 0.7905: zero(9) = 0.7626
    '
    ' assign parameters into dictionary wrapper
    Dim parameters As New Scripting.Dictionary
    parameters.Add HOLEE_PARAMETERS.sigma, sigma
    parameters.Add HOLEE_PARAMETERS.shortRate, shortRate
    parameters.Add HOLEE_PARAMETERS.maturity, maturity
    parameters.Add HOLEE_PARAMETERS.zeroCouponBond, zero
    '
    ' create and initialize calibration model
    Dim model As IModel: Set model = New HoLeeZeroCouponCalibration
    model.initialize parameters
    '
    ' SOLVER part
    Dim Theta(0 To 9) As Double ' assign initial guesses
    Theta(0) = 0.001: Theta(1) = 0.001: Theta(2) = 0.001: Theta(3) = 0.001: Theta(4) = 0.001:
    Theta(5) = 0.001: Theta(6) = 0.001: Theta(7) = 0.001: Theta(8) = 0.001: Theta(9) = 0.001
    '
    Dim numberOfVariables As Long: numberOfVariables = 10
    Dim numberOfEquations As Long: numberOfEquations = 10
    Dim epsilonF As Double: epsilonF = 0.000000000001
    Dim epsilonG As Double: epsilonG = 0.000000000001
    Dim epsilonX As Double: epsilonX = 0.000000000001
    Dim maximumIterations As Long: maximumIterations = 25000
    '
    ' create and initialize solver model
    Dim solver As New AlgLibLMASolver
    solver.initialize _
        numberOfVariables, _
        numberOfEquations, _
        Theta, _
        model, _
        epsilonF, _
        epsilonG, _
        epsilonX, _
        maximumIterations
    '
    ' solve calibration model
    solver.Solve
    '
    ' print hard-coded report containing values for
    ' objective function, variables and other information
    Debug.Print solver.GetPrettyPrintReport
End Sub
'

The results from this calibration model have been verified against the previous results.






















Importing several files into project may involve considerable amount of cruel and unusual repetitive labour. For this specific reason, I have also been woodshedding a separate module (employing VBIDE object), which might give some relief when babysitting those AlgLib modules.

' The following dll libraries need to be referenced :
' Microsoft Visual Basic for Applications Extensibility 5.X, Microsoft Scripting Runtime
Option Explicit
Option Base 0
'
' address to a list, which contains all BAS files which will be included into project
Const listFolderPathName As String = "C:\AlgLib\vba\AlgLibLMAModules.txt"
' address to a folder, which contains all AlgLib BAS files
Const moduleFolderPathName  As String = "C:\AlgLib\vba\alglib-2.6.0.vb6\vb6\src\"
' select TRUE, if Require Variable Declaration in editor is tagged
Const removeOptionExplicitDirective As Boolean = True
'
Public Sub ImportModules()
    '
    ' create a list of modules to be imported into this project
    Dim list() As String: list = createProjectModuleList
    ' import modules into active project
    import list
End Sub
'
Public Sub ExportModules()
    '
    ' create a list of modules to be exported from this project
    Dim list() As String: list = createProjectModuleList
    ' export modules from active project into a defined folder
    export list
End Sub
'
Public Sub RemoveModules()
    '
    ' create a list of modules to be removed from this project
    Dim list() As String: list = createProjectModuleList
    ' delete modules from active project
    remove list
End Sub
'
Private Function import(ByRef list() As String)
    '
    Dim editor As VBIDE.VBProject
    Set editor = ActiveWorkbook.VBProject
    Dim fileSystem As Scripting.FileSystemObject: Set fileSystem = New Scripting.FileSystemObject
    '
    ' loop through all files in a specific source folder for modules
    Dim filesInGivenList As Integer: filesInGivenList = UBound(list) + 1
    If (filesInGivenList = 0) Then Exit Function
    '
    Dim module As VBIDE.VBComponent
    Dim file As Scripting.file
    For Each file In fileSystem.GetFolder(moduleFolderPathName).Files
        '
        ' if there is a given list of specific files to be included
        ' select only the files in that list to be imported into project
        If Not (moduleIsIncluded(file.Name, list)) Then GoTo skipPoint
        '
        Set module = editor.VBComponents.Add(vbext_ct_StdModule)
        If (removeOptionExplicitDirective) Then module.CodeModule.DeleteLines 1
        module.Name = VBA.Split(file.Name, ".")(0)
        module.CodeModule.AddFromFile file.Path
skipPoint:
    Next
End Function
'
Private Function export(ByRef list() As String)
    '
    Dim filesInGivenList As Integer: filesInGivenList = UBound(list) + 1
    If (filesInGivenList = 0) Then Exit Function
    '
    Dim editor As VBIDE.VBProject
    Set editor = ActiveWorkbook.VBProject
    Dim module As VBIDE.VBComponent
    '
    ' loop through all modules
    For Each module In editor.VBComponents
        '
        ' export module only if it is included in the list
        If (moduleIsIncluded(module.Name + ".bas", list)) Then
            module.export moduleFolderPathName + module.Name + ".bas"
        End If
    Next
End Function
'
Private Function remove(ByRef list() As String)
    '
    Dim filesInGivenList As Integer: filesInGivenList = UBound(list) + 1
    If (filesInGivenList = 0) Then Exit Function
    '
    Dim editor As VBIDE.VBProject
    Set editor = ActiveWorkbook.VBProject
    Dim module As VBIDE.VBComponent
    '
    ' loop through all modules
    For Each module In editor.VBComponents
        '
        ' remove module only if it is included in the list
        If (moduleIsIncluded(module.Name + ".bas", list)) Then
            module.Collection.remove module
        End If
    Next
End Function
'
Private Function moduleIsIncluded(ByVal FileName As String, ByRef list() As String) As Boolean
    '
    ' check if a given file name is in the list
    Dim isIncluded As Boolean: isIncluded = False
    Dim i As Integer
    For i = 0 To UBound(list)
        If (FileName = list(i)) Then
            isIncluded = True
            Exit For
        End If
    Next i
    moduleIsIncluded = isIncluded
End Function
'
Private Function createProjectModuleList() As String()
    '
    ' create a list of file names from text file
    Dim fileSystem As Scripting.FileSystemObject: Set fileSystem = New Scripting.FileSystemObject
    Dim fileReader As Scripting.TextStream: Set fileReader = fileSystem.OpenTextFile(listFolderPathName, ForReading)
    Dim fileStreams As String: fileStreams = fileReader.ReadAll
    Dim streams As Variant: streams = VBA.Split(fileStreams, VBA.vbNewLine)
    Dim list() As String: ReDim list(0 To UBound(streams))
    Dim i As Integer
    For i = 0 To UBound(streams)
        list(i) = VBA.Trim(streams(i))
    Next i
    createProjectModuleList = list
End Function
'

Finally, thanks for reading this blog.
-Mike