Showing posts with label Numerical Algorithms. Show all posts
Showing posts with label Numerical Algorithms. Show all posts

Friday, May 5, 2023

MoCaX: Using Chebyshev Tensors for Computational Bottlenecks in Risk Calculations

Even this Machine Learning has been the hottest hype already for years, I have personally still had very vague understanding of how this thing could be applied in risk management domain. This post is presenting one such application: Chebyshev Tensors for getting rid of computational bottlenecks in risk calculations.


All risk management people are familiar with the following generic steps.


The sequence is describing steps for the most types of computationally-intensive risk calculations. In here, pricing (revaluation of financial transactions) is the step, which makes these calculations computationally intensive, since there will usually be substantial amount of revaluation calls sent to all kinds of pricing functions. For this reason, processing times will usually explode and this will costs us time.


In the past, the banks have usually tried to resolve these computational bottlenecks by acquiring more computational muscle and relying on multi-processing schemes. However, for a large enough player with an active derivatives book containing exotic products, even these attempts might not be enough to resolve the issue. Since the burden of producing all kinds of regulatory measures seems to be just growing, there is simply more calculations to be processed, than we have hours within one day.


Chebyshev Tensors


Another way to resolve such computational issues is to find clever algorithmic approximations and that's where Chebyshev Tensors are amazing. In a nutshell, by using Chebyshev Tensors we can create extremely accurate replicas of our standard pricing functions and use them instead of our original time-consuming function in calculations..

Interpolation methods through polynomials can deliver exponential convergence to the original function, when the interpolating points are Chebyshev points. Also, derivatives of the polynomial converge exponentially to the derivatives of the original function. Finally, Chebyshev Tensors can be extended to higher dimensions.


Example: 1-dimensional MoCaX approximation on valuing European call options


First, let us value a portfolio of European call options, where the spot price varies between 50 and 150. We divide this spot price range into one million steps. Other parameters in the model are constants. We get total of one million calculation tasks and one million revaluation calls to our pricing function. Note, that processing time is about 330 seconds (5.5 minutes) to complete all calculations. 


Next, we calculate the approximations for exactly the same set of option values, but we give only 25 analytically-valued option values for Mocax object and the rest of the values will be interpolated by Mocax object. Processing time is about 5.2 seconds to complete all calculation. Maximum error is around 1.5e-12, which is already quite close to machine precision. Error function (analytical values minus approximated values) is shown below. 
















So, with just 25 Chebyshev points in our grid, we get remarkably close to the original pricing function values just with the fraction of original processing time. I dare to describe this as nothing less than impressive. This simple example just quickly scratches the surface, since one can imagine the application areas in risk management calculations are endless. Some of these application areas are well described in the book by Ignacio Ruiz and Mariano Zeron.


Let us go through the code. First, we need to import MoCaX library.


import mocaxpy


After this, we create MoCax object. In constructor, the first argument could be a callable function, but in here we just pass None, since the actual values will be delivered to our model on a later stage. The second argument is the number of dimensions. Since in our pricing function we only change the spot price and all the other variables are being fixed, our model is one-dimensional. The third argument is constructed MocaxDomain object, which sets the domain for our model. In the fourth argument, we can set our error threshold, but in here we just pass None. The fifth argument is constructed MocaxNs object, which sets the accuracy for our model.


n_dimension = 1
n_chebyshev_points = 25
domain = mocaxpy.MocaxDomain([[lowest_spot, highest_spot]])
accuracy = mocaxpy.MocaxNs([n_chebyshev_points])
mocax = mocaxpy.Mocax(None, n_dimension, domain, None, accuracy)


At it's current state, our constructed (but still incomplete) Mocax object is only aware of dimensionality, domain and grid of Chebyshev points. The next step is to set the actual function values for each point in our Chebyshev grid. 


chebyshev_points = mocax.get_evaluation_points()
option_values_at_evaluation_points = [call_option(point[0], 100.0, 1.0, 0.25, 0.01) for point in chebyshev_points]
mocax.set_original_function_values(option_values_at_evaluation_points)
call_options_approximations = [mocax.eval([spot], derivativeId=0) for spot in spots]


First, we request back the information on Chebyshev grid points (total of 25 points) from Mocax object. We need to calculate function values for all these points, since we did not give any callable pricing function in our constructor. Next, we ask revaluations from the original pricing function for all Chebyshev grid points (total of 25 revaluations). Then, we handle these calculated values back to Mocax object. Finally, we request call option values from Mocax object. As a response, we will get one million values back from Mocax object: 25 of those values were being explicitly calculated by using original pricing function and the rest are being interpolated by Mocax object.


The complete example program for this section can be found in my GitHub page.


Example: 2-dimensional MoCaX approximation on valuing European call options


Again, let us value a portfolio of European call options, but this time by changing two variables: spot price and time to maturity. Spot price varies between 50 and 150 and time to maturity varies between 0.25 and 2.5. By dividing the both domains into one thousand steps, we get grid containing one million points and for full revaluation we get total of one million calls to our pricing function. As a result, we get the familiar 3D surface.



















Processing time is about 330 seconds (5.5 minutes) to complete all calculations. 

Next, we calculate the approximations for option values by using Mocax object and 25 Chebyshev points for the both domains. Revaluation calls are being sent for 625 options and the rest will be interpolated by Mocax object. Processing time is about 13.7 seconds to complete all calculation. So, with just 625 Chebyshev points in our grid, we get remarkably close to the original pricing function values just with the fraction of original processing time. Error surface is shown below (maximum error is around 7.7e-08).



 















The complete example program for this section can be found in my GitHub page.


Further material


The main source for all information is MoCaX Intelligence company page and their collection of high-quality YouTube videos. 

The book "Machine Learning for Risk Calculations: A Practitioner's View" written by Ignacio Ruiz and Mariano Zeron is also available for sale.

QuantsHub YouTube channel is also containing 1-hour video on MoCaX, as presented by Ignacio Ruiz. Concerning the material presented in this post, the most essential videos to get started on using MoCaX as presented by Mariano Zeron can be found in here and in here.


The actual MoCaX library (C++ and Python) can be downloaded from software page. The downloaded package is professionally equipped with MoCaX API documentation (doxygen), as well as with program examples and PDF documentations explaining installation and the main usage of MoCaX API. 


Thanks for reading this blog.

-Mike

Sunday, November 17, 2019

QuantLib-Python: Monte Carlo Valuation for Target Accrual Redemption Note

Out of curiosity, I wanted to create an implementation for interest rate Target Accrual Redemption Note (TARN) by using QuantLib-Python library. Now, as one might be aware, the availability of QuantLib Monte Carlo framework for Python is limited (due to templatization of the original C++ classes) to a few existing implementations. This means, that one is not able to re-implement new MC valuation scheme directly by using this specific framework. What to do? There are some workarounds. One may implement such a new scheme by using QuantLib C++ library and then create a wrapper for this library by using SWIG. Some instructions on how to proceed with the SWIG path has also been presented in here. Another way is just to give up the usual QuantLib "Instrument/Engine"-paradigm and just use all the nice pieces available. The complete program can be downloaded from my GitHub page.

TARN


This product is a path-dependent structured note, which terminates when target coupon payment will be reached. When cumulative coupon amount reaches this target amount before maturity, the holder of the note receives final payment and the contract will terminate immediately. TARN coupon payoff is usually structured to be similar to inverse floating rate note. There is (usually) also an attractive teaser coupon attached in the beginning of the structure, combined with the possibility of getting back the par value relatively fast. If the future index will stay low or go even lower, target coupon will be reached early and the investor will enjoy the benefits of high returns for this short-lived investment. However, in the worst case, if the future index path will go sky high, investor will be "bleeding to death" with this long-dated investment yielding inferior returns.

Path Generator


For this specific implementation (for the sake of being as accurate as possible), I gave up using QuantLib Path Generator, because it will create stochastic paths only for even set of points in time. Below is a simple method, which generates paths for a given set of QuantLib dates, which can be unevenly distributed.

# path generator for a given 1d stochastic process and 
# a given set of QuantLib dates, which can be unevenly distributed
# uses process evolve method, which returns asset value after time interval Δt
# returns E(x0,t0,Δt) + S(x0,t0,Δt) ⋅ Δw, where E is expectation and S standard deviation
# input arguments:
#   x0 = asset value at inception
#   dates = array of dates
#   dayCounter = QuantLib day counter
#   process = QuantLib 1D stochastic process implementation
#   nPaths = number of paths to be simulated
def PathGenerator(x0, dates, dayCounter, process, nPaths):
    t = np.array([dayCounter.yearFraction(dates[0], d) for d in dates])    
    urg = ql.UniformRandomGenerator()
    ursg = ql.UniformRandomSequenceGenerator(t.shape[0] - 1, urg)
    grsg = ql.GaussianRandomSequenceGenerator(ursg)    
    paths = np.zeros(shape = (nPaths, t.shape[0]))
    
    for j in range(nPaths):
        dw = np.array(list(grsg.nextSequence().value()))
        x = x0
        path = []

        for i in range(1, t.shape[0]):
            x = process.evolve(t[i-1], x, (t[i] - t[i-1]), dw[i-1])
            path.append(x)
            
        path = np.hstack([np.array([x0]), np.array(path)])
        paths[j,:] = path
        
    # return array dimensions: [number of paths, number of items in t array]
    return paths

Main Program


First, we create the usual set of required QuantLib parameters, such as valuation date. For the sake of being able to value this product also after its inception, valuation method takes QuantLib Index object as one argument. All past fixing dates and rates can be stored into this index. Now, if one changes valuation date (currently 4.3.2018) to any possible date after inception and before transaction maturity, the product will be valued accordingly. It should be noted, that in such scheme it is crucial to provide all possible past fixings. Failure to do this, will lead to an exception.

For creating valuation curve (curveHandle) and short rate process (HW1F), I have decided to take the shortest possible way and just created flat forward curve and assumed constants for short rate process parameters (mean reversion and short rate volatility), instead calibrating this model into the actual market data. For any "real-life" purposes, one may take a look at some possible proper implementations given in here for valuation curve and in here for model calibration.

Next, all transaction parameters are being created one by one. Coupon dates are being created by using QuantLib Schedule object. Finally, PV and the average termination time point is requested from TARN method and printed.

import QuantLib as ql
import numpy as np

# define general QuantLib-related parameters
valuationDate = ql.Date(4,3,2018)
calendar = ql.TARGET()
convention = ql.ModifiedFollowing
dayCounter = ql.Actual360()
ql.Settings.instance().evaluationDate = valuationDate

# set index object and past fixings
pastFixingsDates = np.array([ql.Date(4,3,2019), ql.Date(4,3,2020)])
pastFixingsRates = np.array([0.05, 0.05])
index = ql.USDLibor(ql.Period(12, ql.Months))
index.clearFixings()
index.addFixings(pastFixingsDates, pastFixingsRates)

# create discounting curve and process for short rate
r0 = 0.015
curveHandle = ql.YieldTermStructureHandle(ql.FlatForward(valuationDate, r0, dayCounter))
a = 0.05
vol = 0.009
HW1F = ql.HullWhiteProcess(curveHandle, a, vol)

# define bond-related parameters
startDate = ql.Date(4,3,2018)
firstCouponDate = calendar.advance(startDate, ql.Period(1, ql.Years))
lastCouponDate = calendar.advance(startDate, ql.Period(10, ql.Years))
couponDates = np.array(list(ql.Schedule(firstCouponDate, lastCouponDate, ql.Period(ql.Annual), 
    calendar, ql.ModifiedFollowing, ql.ModifiedFollowing, ql.DateGeneration.Forward, False)))
teaserCoupon = np.array([0.1])
targetCoupon = 0.25
hasToReachTarget = True
cap = 0.15
floor = 0.0
fixedRate = 0.1
factor = 3.0
structuredCouponPayoff = lambda r: max(fixedRate - factor * r, 0.0)
notional = 1000000.0

# define monte carlo-related parameters
nPaths = 10000

# request result (PV and average termination point)
result = TARN(startDate, valuationDate, couponDates, targetCoupon, teaserCoupon,
    cap, floor, hasToReachTarget, structuredCouponPayoff, notional, dayCounter,
    nPaths, HW1F, curveHandle, index)

print('pv', '{0:.0f}'.format(result[0]))
print('termination', '{0:.1f}'.format(result[1]))

Valuation Method


The last piece of code shows the actual pricing method. Paths will be generated for all remaining coupon dates. Past fixing rates will be used for all such coupon dates, which happened in the past.

All simulated paths will then be processed in the loop. Thanks to amazing variety of different types of array operations in Numpy Array library, we can process a path without deploying any typical "inner loop" (for path steps). The loop basically starts with a given set of simulated rates and along the way, transforms this path into an array of cash flow present values. Operations are commented in the code.

def TARN(bondStartDate, valuationDate, couponDates, targetCoupon, teaserCoupon, cap, floor,
    hasToReachTarget, payoff, notional, dayCounter, nPaths, process, curve, index):    
    
    # immediate exit trigger for matured transaction
    if(valuationDate >= couponDates[-1]):
        return (0.0, 0.0)
    
    # create date array for path generator
    # combine valuation date and all remaining coupon dates
    dates = np.hstack((np.array([valuationDate]), couponDates[couponDates > valuationDate]))
    
    # generate paths for a given set of dates, exclude the current spot rate
    paths = PathGenerator(process.x0(), dates, dayCounter, process, nPaths)[:,1:]
    
    # identify past coupon dates
    pastDates = couponDates[couponDates <= valuationDate]
    # conditionally, merge given past fixings and generated paths
    if(pastDates.shape[0] > 0):
        pastFixings = np.array([index.fixing(pastDate) for pastDate in pastDates])    
        pastFixings = np.tile(pastFixings, (paths.shape[0], 1))
        paths = np.hstack((pastFixings, paths))
        
    # define time grid for all coupon dates, calculate day count fractions
    t = np.array([0.0] + [dayCounter.yearFraction(bondStartDate, d) for d in couponDates])
    dcf = np.diff(t)
    
    # result accumulators
    global_pv = []
    termination = []

    # calculate PV for all paths
    for path in paths:
        # transform simulated path into structured rates using payoff function
        path = (np.vectorize(payoff))(path)
        index_1 = np.where(teaserCoupon > 0.0)
        # replace some path rates with teaser coupons (if exists)
        path[index_1] = teaserCoupon
        # calculate capped and floored structured coupon for non-teaser rates
        path = np.concatenate([path[index_1], np.minimum(path[index_1[0].shape[0]:], cap)])
        path = np.concatenate([path[index_1], np.maximum(path[index_1[0].shape[0]:], floor)])
        # multiply rates with day count fractions
        path = np.multiply(path, dcf)
        # take into account only rates, for which cumulative sum is less or equal to target coupon
        index_2 = np.where(np.cumsum(path) <= targetCoupon)
        path = path[index_2]
        dates = couponDates[index_2]
        # path termination time is the date, which reaches target coupon
        termination.append(dayCounter.yearFraction(valuationDate, dates[-1]))
        # if coupon has to reach target coupon, add remaining coupon available into final coupon
        if(hasToReachTarget): path[-1] = targetCoupon - np.sum(path[:-1])
        # multiply coupon rates with notionals, add final redemption
        path *= notional
        path[-1] += notional
        # take into account only coupons, for which coupon dates are in the future
        index_3 = np.where(dates >= valuationDate)
        dates = dates[index_3]
        path = path[index_3]
        # request discount factors for all coupon dates
        df = np.array([curve.discount(d) for d in dates])
        # calculate coupon PV's
        path = np.multiply(path, df)
        # add path PV into result accumulator
        global_pv.append(np.sum(path))

    # return tuple (pv, average termination time)
    return (np.mean(np.array(global_pv)), np.mean(np.array(termination)))

A couple of final notes. The presented example is valuing a product in which the coupon is exposed to interest rates. However, a notable amount of TARN products issued (in the past), have been exposing their coupons to movements in FX rates. Since any one-dimensional process can be used for path generation purposes in the valuation method, there is a possibility to use some of the existing QuantLib processes also for modeling the path of future FX rates. Also, structured coupon payoff can be defined outside the valuation method. Since all types of TARN products are (usually) pretty much sharing the same other properties (ex. path-dependency, teaser rates), the valuation method is relatively "generic" after all.

Thanks for reading this blog.
-Mike

Sunday, October 20, 2019

Python-QuantLib-SciPy: Optimizing Smooth Libor Forward Curve Revisited

One reader was making a remark, that my implementation for curve calibration scheme as presented in here, was not implemented by using QuantLib. As I was re-thinking my implementation, I suddenly remembered that in QuantLib, there are actually several ways to create yield term structures. One such approach is to create yield term structure from a set of given dates and forward rates. Also, VanillaSwap class gives us a direct mechanism for valuing swap transaction by using constructed curve. Bingo. So, this post is re-visiting curve calibration scheme, but this time implemented by using relevant QuantLib-Python library tools.

A few words about utility classes. Convert class will be used for transforming specific in-built data types into specific QuantLib types (Date, Calendar, DayCounter, etc). VanillaSwap class is just a wrapper for corresponding QuantLib.VanillaSwap class. It should be noted, that there is conventional NPV method for requesting swap PV by giving two specific arguments (yield term structure and floating leg index). There is also another method NPV_calibration, which is used for calculating swap PV during calibration process. For this method, input arguments are list of forward rates, list of forward dates and list of initial market rates. From this information, specific yield term structure implementation (QuantLib.ForwardCurve) will be created and used for valuing swap transaction.

import re
import numpy as np
import scipy.optimize as opt
import matplotlib.pyplot as pl
import QuantLib as ql

# utility class for different QuantLib type conversions 
class Convert():
    
    # convert date string ('yyyy-mm-dd') to QuantLib Date object
    def to_date(s):
        monthDictionary = {
            '01': ql.January, '02': ql.February, '03': ql.March,
            '04': ql.April, '05': ql.May, '06': ql.June,
            '07': ql.July, '08': ql.August, '09': ql.September,
            '10': ql.October, '11': ql.November, '12': ql.December
        }
        arr = re.findall(r"[\w']+", s)
        return ql.Date(int(arr[2]), monthDictionary[arr[1]], int(arr[0]))
    
    # convert string to QuantLib businessdayconvention enumerator
    def to_businessDayConvention(s):
        if (s.upper() == 'FOLLOWING'): return ql.Following
        if (s.upper() == 'MODIFIEDFOLLOWING'): return ql.ModifiedFollowing
        if (s.upper() == 'PRECEDING'): return ql.Preceding
        if (s.upper() == 'MODIFIEDPRECEDING'): return ql.ModifiedPreceding
        if (s.upper() == 'UNADJUSTED'): return ql.Unadjusted
        
    # convert string to QuantLib calendar object
    def to_calendar(s):
        if (s.upper() == 'TARGET'): return ql.TARGET()
        if (s.upper() == 'UNITEDSTATES'): return ql.UnitedStates()
        if (s.upper() == 'UNITEDKINGDOM'): return ql.UnitedKingdom()
        # TODO: add new calendar here
        
    # convert string to QuantLib swap type enumerator
    def to_swapType(s):
        if (s.upper() == 'PAYER'): return ql.VanillaSwap.Payer
        if (s.upper() == 'RECEIVER'): return ql.VanillaSwap.Receiver
        
    # convert string to QuantLib frequency enumerator
    def to_frequency(s):
        if (s.upper() == 'DAILY'): return ql.Daily
        if (s.upper() == 'WEEKLY'): return ql.Weekly
        if (s.upper() == 'MONTHLY'): return ql.Monthly
        if (s.upper() == 'QUARTERLY'): return ql.Quarterly
        if (s.upper() == 'SEMIANNUAL'): return ql.Semiannual
        if (s.upper() == 'ANNUAL'): return ql.Annual

    # convert string to QuantLib date generation rule enumerator
    def to_dateGenerationRule(s):
        if (s.upper() == 'BACKWARD'): return ql.DateGeneration.Backward
        if (s.upper() == 'FORWARD'): return ql.DateGeneration.Forward
        # TODO: add new date generation rule here

    # convert string to QuantLib day counter object
    def to_dayCounter(s):
        if (s.upper() == 'ACTUAL360'): return ql.Actual360()
        if (s.upper() == 'ACTUAL365FIXED'): return ql.Actual365Fixed()
        if (s.upper() == 'ACTUALACTUAL'): return ql.ActualActual()
        if (s.upper() == 'ACTUAL365NOLEAP'): return ql.Actual365NoLeap()
        if (s.upper() == 'BUSINESS252'): return ql.Business252()
        if (s.upper() == 'ONEDAYCOUNTER'): return ql.OneDayCounter()
        if (s.upper() == 'SIMPLEDAYCOUNTER'): return ql.SimpleDayCounter()
        if (s.upper() == 'THIRTY360'): return ql.Thirty360()
        
    # convert string (ex.'USDLibor.3M') to QuantLib ibor index object
    # note: forwarding term structure has to be linked to index object separately
    def to_iborIndex(s):
        s = s.split('.')
        if(s[0].upper() == 'USDLIBOR'): return ql.USDLibor(ql.Period(s[1]))
        if(s[0].upper() == 'EURIBOR'): return ql.Euribor(ql.Period(s[1]))  

class VanillaSwap(object):
    
    def __init__(self, ID, swapType, nominal, startDate, maturityDate, fixedLegFrequency, 
        fixedLegCalendar, fixedLegConvention, fixedLegDateGenerationRule, fixedLegRate, fixedLegDayCount,
        fixedLegEndOfMonth, floatingLegFrequency, floatingLegCalendar, floatingLegConvention, 
        floatingLegDateGenerationRule, floatingLegSpread, floatingLegDayCount, 
        floatingLegEndOfMonth, floatingLegIborIndex):

        # create member data, convert all required QuantLib types
        self.ID = str(ID)
        self.swapType = Convert.to_swapType(swapType)
        self.nominal = float(nominal)
        self.startDate = Convert.to_date(startDate)
        self.maturityDate = Convert.to_date(maturityDate)
        self.fixedLegFrequency = ql.Period(Convert.to_frequency(fixedLegFrequency))
        self.fixedLegCalendar = Convert.to_calendar(fixedLegCalendar)
        self.fixedLegConvention = Convert.to_businessDayConvention(fixedLegConvention)
        self.fixedLegDateGenerationRule = Convert.to_dateGenerationRule(fixedLegDateGenerationRule)
        self.fixedLegRate = float(fixedLegRate)
        self.fixedLegDayCount = Convert.to_dayCounter(fixedLegDayCount)
        self.fixedLegEndOfMonth = bool(fixedLegEndOfMonth)
        self.floatingLegFrequency = ql.Period(Convert.to_frequency(floatingLegFrequency))
        self.floatingLegCalendar = Convert.to_calendar(floatingLegCalendar)
        self.floatingLegConvention = Convert.to_businessDayConvention(floatingLegConvention)
        self.floatingLegDateGenerationRule = Convert.to_dateGenerationRule(floatingLegDateGenerationRule)
        self.floatingLegSpread = float(floatingLegSpread)
        self.floatingLegDayCount = Convert.to_dayCounter(floatingLegDayCount)
        self.floatingLegEndOfMonth = bool(floatingLegEndOfMonth)
        self.floatingLegIborIndex = Convert.to_iborIndex(floatingLegIborIndex)

        # create fixed leg schedule
        self.fixedLegSchedule = ql.Schedule(
            self.startDate, 
            self.maturityDate,
            self.fixedLegFrequency, 
            self.fixedLegCalendar, 
            self.fixedLegConvention,
            self.fixedLegConvention,
            self.fixedLegDateGenerationRule,
            self.fixedLegEndOfMonth)
        
        # create floating leg schedule
        self.floatingLegSchedule = ql.Schedule(
            self.startDate, 
            self.maturityDate,
            self.floatingLegFrequency, 
            self.floatingLegCalendar, 
            self.floatingLegConvention,
            self.floatingLegConvention,
            self.floatingLegDateGenerationRule,
            self.floatingLegEndOfMonth)

    # NPV method used for specific calibration purposes
    # x = list of forward rates
    # args: 0 = list of forward dates, 1 = list of market rates        
    def NPV_calibration(self, x, args):
        # concatenate given market rates and given forward rates
        x = np.concatenate([args[1], x])
        
        # create QuantLib yield term structure object
        # from a given set of forward rates and dates
        curve = ql.YieldTermStructureHandle(ql.ForwardCurve(args[0], x, 
            self.floatingLegDayCount, self.floatingLegCalendar))        

        # set forwarding term structure to floating leg index
        self.floatingLegIborIndex = self.floatingLegIborIndex.clone(curve) 
        
        # create vanilla interest rate swap
        self.instrument = ql.VanillaSwap(
            self.swapType, 
            self.nominal, 
            self.fixedLegSchedule, 
            self.fixedLegRate, 
            self.fixedLegDayCount, 
            self.floatingLegSchedule,
            self.floatingLegIborIndex,
            self.floatingLegSpread, 
            self.floatingLegDayCount)
        
        # pair instrument with pricing engine, request PV
        self.instrument.setPricingEngine(ql.DiscountingSwapEngine(curve))
        return self.instrument.NPV()
    
    # NPV method used for general pricing purposes
    def NPV(self, yieldTermStructureHandle, floatingLegIborIndex):
        # set forwarding term structure to floating leg index
        self.floatingLegIborIndex = floatingLegIborIndex.clone(yieldTermStructureHandle)
        
        # create vanilla interest rate swap
        self.instrument = ql.VanillaSwap(
            self.swapType, 
            self.nominal, 
            self.fixedLegSchedule, 
            self.fixedLegRate, 
            self.fixedLegDayCount, 
            self.floatingLegSchedule,
            self.floatingLegIborIndex,
            self.floatingLegSpread, 
            self.floatingLegDayCount)
        
        # pair instrument with pricing engine, request PV
        self.instrument.setPricingEngine(ql.DiscountingSwapEngine(yieldTermStructureHandle))
        return self.instrument.NPV()

ObjectiveFunction is used for minimization process for calculating sum of squared errors of all adjacent forward rates. Maximum smoothness of the curve will be achieved as a result of this minimization.

# objective function calculates sum of squared errors of all decision variables
# x = list of forward rates
# args: 0 = list of market rates data, 1 = scaling factor
def ObjectiveFunction(x, args):
    # concatenate given market rates and forward rates
    x = np.concatenate([args[0], x])
    return np.sum(np.power(np.diff(x), 2) * args[1])

The actual program flow will start by creating a set of vanilla interest rate swap objects (VanillaSwap) into list.

# dynamic data parts for set of vanilla swaps 
swapIDs = ['2Y', '3Y', '4Y', '5Y', '6Y', '7Y', '8Y', '9Y', '10Y', '12Y', '15Y', '20Y', '25Y', '30Y']
maturities = ['2010-02-08', '2011-02-07', '2012-02-06', '2013-02-06', '2014-02-06', '2015-02-06', '2016-02-08', 
    '2017-02-06', '2018-02-06', '2020-02-06', '2023-02-06', '2028-02-07', '2033-02-07', '2038-02-08']
swapRates = [0.02795, 0.03035, 0.03275, 0.03505, 0.03715, 0.03885, 0.04025, 0.04155, 0.04265, 0.04435, 
    0.04615, 0.04755, 0.04805, 0.04815]

# create vanilla swap transaction objects into list
swaps = [VanillaSwap(swapID, 'Payer', 1000000, '2008-02-06', maturity, 'Annual', 
    'Target', 'ModifiedFollowing', 'Backward', swapRate, 'Actual360', False, 'Quarterly', 
    'Target', 'ModifiedFollowing', 'Backward', 0.0, 'Actual360', False, 'USDLibor.3M')
    for swapID, maturity, swapRate in zip(swapIDs, maturities, swapRates)]

Next, the program will initialize the actual market data (to be used in forward curve), starting values for forward curve, set of dates for forward curve, optimization constraints and the actual optimization model.

# take initial forward rates from market data, set initial guesses and scaling factor for objective function
initialMarketData = np.array([0.03145, 0.0279275, 0.0253077, 0.0249374])
initialForwardGuesses = np.full(117, 0.02)
scalingFactor = 1000000.0

# create relevant QuantLib dates
today = ql.Date(4, 2, 2008)
ql.Settings.instance().evaluationDate = today  
settlementDate = ql.TARGET().advance(today, ql.Period(2, ql.Days))

# create set of dates for forward curve
dates = list(ql.Schedule(settlementDate, ql.Date(6, 2, 2038), ql.Period(ql.Quarterly), ql.TARGET(),
    ql.ModifiedFollowing, ql.ModifiedFollowing, ql.DateGeneration.Backward, False))

# create constraints for optimization model
swapConstraints = tuple([{'type': 'eq', 'fun': swap.NPV_calibration, 'args': [[dates, initialMarketData]]} for swap in swaps])

# create and execute scipy minimization model
model = opt.minimize(ObjectiveFunction, initialForwardGuesses, args = ([initialMarketData, scalingFactor]), 
    method = 'SLSQP', options = {'maxiter': 500}, constraints = swapConstraints)

After processing optimization model, the actual calibrated forward rates can be requested from the model. Next part of the program will just plot the calibrated forward term structure.

# extract calibrated forward rates, create times and plot term structure
forwards = np.concatenate([initialMarketData, model.x])
times = np.array([ql.Actual360().yearFraction(settlementDate, date) for date in dates])
pl.plot(times, forwards)
pl.show()















Final part of the program will create QuantLib yield term structure (QuantLib.ForwardCurve) and re-values our initial set of swap transactions. All swaps will be priced to zero at inception date.

# create new QuantLib curve from calibrated forward rates
curve = ql.YieldTermStructureHandle(ql.ForwardCurve(dates, forwards, ql.Actual360(), ql.TARGET()))

# value initial set of vanilla swaps using new QuantLib valuation curve
# all swaps will be priced to zero at inception date
for swap in swaps:
    index = ql.USDLibor(ql.Period(3, ql.Months), curve)
    pv = swap.NPV(curve, index)
    print(swap.ID, '{0:.5f}'.format(pv))

It should be noted, that by utilizing calibration scheme presented in this post, valuation curves (yield term structures) for QuantLib for all currencies and for all different basis can be constructed (assuming the relevant market data exists). Next logical step would be to implement similar scheme for calibrating valuation curves for QuantLib for cross-currency cases and within collateralized world.

Complete program can be downloaded from my GitHub page. Example data has been taken from chapter three within excellent book by Richard Flavell. Finally, thanks again for reading this blog.
-Mike

Wednesday, May 1, 2019

Python-SciPy: Optimizing Smooth Libor Forward Curve

In my previous post, I presented how to use Python SciPy Optimization package for solving zero-coupon rate term structure from a given set of zero-coupon bond prices numerically. In this post, the same approach will be used in order to solve smooth Libor forward curve from a given set of vanilla swaps. Example of applying this scheme using Solver and Excel/VBA can be found in here.

In order to value fixed income derivatives cash flows, relevant forward rates and discount factors have to be defined from bootstrapped zero-coupon curve. In a Libor world, we use cash and FRA contracts (or futures contracts) in a short-end of the curve, while in a long-end of the curve we use swap contracts. Let us assume for a moment, that we bootstrap USD 3M zero-coupon curve, in order to get 3M forward rates on a quarterly basis. While bootstrapping approach usually gives smooth curve within short-end of the curve, it will usually fail to do this within long-end of the curve. This happens, because there is no swap contracts available in a long-end of the curve and hereby, we need to do a lot of interpolations for required swap rates. The resulting forward curve then looks like a chain of waterfalls and for the long-end of the curve, smoothness will be lost. This topic has been thoroughly covered in chapter three within this excellent book by Richard Flavell.

In a nutshell, we first define the shortest end of the curve (using spot cash rate) and then we just guess the rest of the forward rates. After this, we minimize sum of squared differences between adjacent forward rates, subject to constraints. In this case, constraints are present values of swaps, which by definition will be zero. When this optimization task has been performed, the resulting forward curve will successfully re-price the current swap market.
















Bloomberg forward curve screen for the same set of data is available in here. Finally, thanks again for reading my blog.
-Mike

import numpy as np
import scipy.optimize as opt
import matplotlib.pyplot as pl

# sum of squared errors of all decision variables
# args: 0 = array of given initial rates, 1 = scaling factor
def ObjectiveFunction(x, args):
    # concatenate given initial rates and given 'guesses' for forward rates
    x = np.concatenate([args[0], x])
    return np.sum(np.power(np.diff(x), 2) * args[1])

# x = array of Libor forward rates
# args: 0 = swap rate, 1 = years to maturity, 2 = floating leg payments per year, 
# 3 = notional, 4 = array of given initial rates
def VanillaSwapPV(x, args):
    # PV (fixed payer swap) = -swapRate * Q(T) + (1 - DF(T))
    # where Q(T) is sumproduct of fixed leg discount factors and corresponding time accrual factors
    # assumption : fixed leg is always paid annually
    # since fixed leg is paid annually and maturity is always integer, 
    # time accrual factor will always be exactly 1.0
    
    # concatenate given initial rates and given 'guesses' for forward rates
    x = np.concatenate([args[4], x])
    DF = 0.0
    Q  = 0.0
    floatingLegTenor = 1 / args[2]
    nextFixedLegCouponDate = 1
    currentTimePoint = 0.0
    nCashFlows = int(args[1] * args[2])
    
    for i in range(nCashFlows):
        currentTimePoint += floatingLegTenor
        # first rate is always spot rate: calculate first spot discount factor
        if (i == 0):
            DF = 1 / (1 + x[i] * floatingLegTenor)
        # other rates are always forward rates
        if (i > 0):
            # solve current spot discount factor using previous spot discount factor
            # and current forward discount factor: DF(0,T) = DF(0,t) * DF(t,T), where (0 < t < T)
            DF *= (1 / (1 + x[i] * floatingLegTenor))
            if ((currentTimePoint - nextFixedLegCouponDate) == 0):
                Q += DF
                nextFixedLegCouponDate += 1
                
    return (-args[0] * Q + (1.0 - DF)) * args[3]

# array of given initial rates (the first given rate has to be spot cash rate)
# here, we give only one: 3M spot Libor fixing rate ('cash-to-first-future')
initialRates = np.array([0.006276])
scalingFactor = 1000000.0

# vanilla swap pricing functions as constraints
# args: swapRate, yearsToMaturity, floatingLegPaymentFrequency, notional, array of given initial rates
swaps = ({'type': 'eq', 'fun': VanillaSwapPV, 'args': [[0.0078775, 1, 4, scalingFactor, initialRates]]},
        {'type': 'eq', 'fun': VanillaSwapPV, 'args': [[0.009295, 2, 4, scalingFactor, initialRates]]},
        {'type': 'eq', 'fun': VanillaSwapPV, 'args': [[0.0103975, 3, 4, scalingFactor, initialRates]]},
        {'type': 'eq', 'fun': VanillaSwapPV, 'args': [[0.01136, 4, 4, scalingFactor, initialRates]]},
        {'type': 'eq', 'fun': VanillaSwapPV, 'args': [[0.012268, 5, 4, scalingFactor, initialRates]]},
        {'type': 'eq', 'fun': VanillaSwapPV, 'args': [[0.013183, 6, 4, scalingFactor, initialRates]]},
        {'type': 'eq', 'fun': VanillaSwapPV, 'args': [[0.01404, 7, 4, scalingFactor, initialRates]]},
        {'type': 'eq', 'fun': VanillaSwapPV, 'args': [[0.014829, 8, 4, scalingFactor, initialRates]]},
        {'type': 'eq', 'fun': VanillaSwapPV, 'args': [[0.01554, 9, 4, scalingFactor, initialRates]]},
        {'type': 'eq', 'fun': VanillaSwapPV, 'args': [[0.016197, 10, 4, scalingFactor, initialRates]]},
        {'type': 'eq', 'fun': VanillaSwapPV, 'args': [[0.016815, 11, 4, scalingFactor, initialRates]]},
        {'type': 'eq', 'fun': VanillaSwapPV, 'args': [[0.017367, 12, 4, scalingFactor, initialRates]]},
        {'type': 'eq', 'fun': VanillaSwapPV, 'args': [[0.018645, 15, 4, scalingFactor, initialRates]]},
        {'type': 'eq', 'fun': VanillaSwapPV, 'args': [[0.01996, 20, 4, scalingFactor, initialRates]]},
        {'type': 'eq', 'fun': VanillaSwapPV, 'args': [[0.020615, 25, 4, scalingFactor, initialRates]]},
        {'type': 'eq', 'fun': VanillaSwapPV, 'args': [[0.02097, 30, 4, scalingFactor, initialRates]]},
        {'type': 'eq', 'fun': VanillaSwapPV, 'args': [[0.021155, 40, 4, scalingFactor, initialRates]]},
        {'type': 'eq', 'fun': VanillaSwapPV, 'args': [[0.021017, 50, 4, scalingFactor, initialRates]]})

# initial guesses for Libor forward rates up to 50 years
# number of rates: 50 * 4 - 1 (years to maturity * floating leg payments per year - first given cash spot rate)
initialGuesses = np.full(199, 0.006)
model = opt.minimize(ObjectiveFunction, initialGuesses, args = ([initialRates, scalingFactor]), method = 'SLSQP', options = {'maxiter': 500}, constraints = swaps)

# print selected model results
print('Success: ' + str(model.success))
print('Message: ' + str(model.message))
print('Number of iterations: ' + str(model.nit))
print('Objective function (sum of squared errors): ' + str(model.fun))
#print('Changing variables (Libor forward rates): ' + str(model.x * 100))
pl.plot(model.x)
pl.show()

Tuesday, April 30, 2019

Python-SciPy: Solving Zero-Coupon Term Structure Using Optimization

This post is presenting how to use Python SciPy Optimization package for solving out zero-coupon rate term structure from a given set of zero-coupon bond prices. Needless to say, we do not need any numerical method to do this, since we have exact analytical formulas for backing out zero-coupon rates from zero-coupon bond prices. However, approach presented in this relatively simple example can be applied into more interesting schemes, such as solving out smooth Libor forward curve from a given set of vanilla swaps. Example of such scheme using Solver and Excel/VBA can be found in here.

Let us assume zero-coupon rate term structure as shown below. Let us also assume, that the only information we can observe is the zero-coupon bond prices. Our task is to solve zero-coupon rates term structure.
















In a nutshell, we first set initial guesses for ten zero-coupon rates. After this we minimize sum of squared differences between adjacent zero-coupon rates (in objective function), but subject to constraints. In this case, constraints are differences between observed zero-coupon bond market prices and calculated bond prices, which need to be pushed to zero. When this optimization task has been completed, the resulting zero-coupon term structure will successfully re-price the current zero-coupon bond market, while curve smoothness is maximized. Example program results are shown below.


















Thanks for reading this blog.
-Mike

import numpy as np
import scipy.optimize as opt
import matplotlib.pyplot as pl

# sum of squared errors of decision variables
def ObjectiveFunction(x, args):
    return np.sum(np.power(np.diff(x), 2) * args[0])

# zero coupon bond pricing function
def ZeroCouponBond(x, args):
    # return difference between calculated and market price
    return ((1 / (1 + x[args[3]])**args[2]) - args[0]) * args[1]

# zero coupon bond pricing functions as constraints
# args: market price, scaling factor, maturity, index number for rate array
zeroPrices = ({'type': 'eq', 'fun': ZeroCouponBond, 'args': [[0.998801438274071, 1000000.0, 1.0, 0]]},
            {'type': 'eq', 'fun': ZeroCouponBond, 'args': [[0.996210802629012, 1000000.0, 2.0, 1]]},
            {'type': 'eq', 'fun': ZeroCouponBond, 'args': [[0.991943543964159, 1000000.0, 3.0, 2]]},
            {'type': 'eq', 'fun': ZeroCouponBond, 'args': [[0.981028206597786, 1000000.0, 4.0, 3]]},
            {'type': 'eq', 'fun': ZeroCouponBond, 'args': [[0.962851266220459, 1000000.0, 5.0, 4]]},
            {'type': 'eq', 'fun': ZeroCouponBond, 'args': [[0.946534719794057, 1000000.0, 6.0, 5]]},
            {'type': 'eq', 'fun': ZeroCouponBond, 'args': [[0.924997530805076, 1000000.0, 7.0, 6]]},
            {'type': 'eq', 'fun': ZeroCouponBond, 'args': [[0.912584111300984, 1000000.0, 8.0, 7]]},
            {'type': 'eq', 'fun': ZeroCouponBond, 'args': [[0.892632531026722, 1000000.0, 9.0, 8]]},
            {'type': 'eq', 'fun': ZeroCouponBond, 'args': [[0.877098137542374, 1000000.0, 10.0, 9]]})

# initial guesses for ten zero-coupon rates
initialGuess = np.full(10, 0.005)
model = opt.minimize(ObjectiveFunction, initialGuess, args = ([1000000.0]), method = 'SLSQP', constraints = zeroPrices)

# print selected model results
print('Success: ' + str(model.success))
print('Message: ' + str(model.message))
print('Number of iterations: ' + str(model.nit))
print('Objective function (sum of squared errors): ' + str(model.fun))
print('Changing variables (zero-coupon rates): ' + str(model.x * 100))
pl.plot(model.x)
pl.show()

Sunday, November 25, 2018

QuantLib-Python: Hull-White one-factor model calibration

This Python program is presenting the process of calibrating Hull-White One-factor interest rate model to a given set of Swaption volatilities. In the example program, I have used exactly the same data as used in the book QuantLib Python Cookbook by G. Balaraman and L. Ballabio in the Chapter 14 (Short Interest Rate Model Calibration) and corresponding results are perfectly replicated here.











Thanks for reading my blog.
-Mike

from QuantLib import *
import numpy as Numpy

# hard-coded data
def CreateSwaptionVolatilityList():
    vol = []
    vol.append(0.1148)
    vol.append(0.1108)
    vol.append(0.1070)
    vol.append(0.1021)
    vol.append(0.1000)
    return vol

class ModelCalibrator:
    def __init__(self, endCriteria):        
        self.endCriteria = endCriteria
        self.helpers = []

    def AddCalibrationHelper(self, helper):
        self.helpers.append(helper)

    def Calibrate(self, model, engine, curve, fixedParameters):
        # assign pricing engine to all calibration helpers
        for i in range(len(self.helpers)):
            self.helpers[i].setPricingEngine(engine)
        method = LevenbergMarquardt()
        if(len(fixedParameters) == 0):
            model.calibrate(self.helpers, method, self.endCriteria)
        else:
            model.calibrate(self.helpers, method, self.endCriteria,
                NoConstraint(), [], fixedParameters)

# general parameters
tradeDate = Date(15, February, 2002)
Settings.instance().evaluationDate = tradeDate
settlementDate = Date(19, February, 2002)
calendar = TARGET()
dayCounter = Actual360()

# create market data: term structure and diagonal volatilities
curveHandle = YieldTermStructureHandle(FlatForward(settlementDate, 0.04875825, Actual365Fixed()))
vol = CreateSwaptionVolatilityList()

# create calibrator object
endCriteria = EndCriteria(10000, 100, 0.000001, 0.00000001, 0.00000001)
calibrator = ModelCalibrator(endCriteria)

# add swaption helpers to calibrator
for i in range(len(vol)):
    t = i + 1; tenor = len(vol) - i    
    helper = SwaptionHelper(
        Period(t, Years), 
        Period(tenor, Years), 
        QuoteHandle(SimpleQuote(vol[i])), 
        USDLibor(Period(3, Months), curveHandle), 
        Period(1, Years), 
        dayCounter, 
        dayCounter, 
        curveHandle)    
    calibrator.AddCalibrationHelper(helper)

# create model and pricing engine, calibrate model and print calibrated parameters
print('case 1 : calibrate all involved parameters (HW1F : reversion, sigma)')
model = HullWhite(curveHandle)
engine = JamshidianSwaptionEngine(model)
fixedParameters = []
calibrator.Calibrate(model, engine, curveHandle, fixedParameters)
print('calibrated reversion: ' + str(round(model.params()[0], 5)))
print('calibrated sigma: ' + str(round(model.params()[1], 5)))
print()

print('case 2 : calibrate sigma and fix reversion to 0.05')
model = HullWhite(curveHandle, 0.05, 0.0001)
engine = JamshidianSwaptionEngine(model)
fixedParameters = [True, False]
calibrator.Calibrate(model, engine, curveHandle, fixedParameters)
print('fixed reversion: ' + str(round(model.params()[0], 5)))
print('calibrated sigma: ' + str(round(model.params()[1], 5)))
print()

print('case 3 : calibrate reversion and fix sigma to 0.01')
model = HullWhite(curveHandle, 0.05, 0.01)
engine = JamshidianSwaptionEngine(model)
fixedParameters = [False, True]
calibrator.Calibrate(model, engine, curveHandle, fixedParameters)
print('calibrated reversion: ' + str(round(model.params()[0], 5)))
print('fixed sigma: ' + str(round(model.params()[1], 5)))
print()

Wednesday, October 10, 2018

Wilmott : Software Interoperability in Computational Finance, Part II: Applications to Derivatives Pricing in C++11 and C#

"With an anxiety that almost amounted to agony, I collected the instruments of life around me, that I might infuse a spark of being into the lifeless thing that lay at my feet. It was already one in the morning; the rain pattered dismally against the panes, and my candle was nearly out, when, by the glimmer of the half-extinguished light, I saw the dull yellow eye of the creature open; it breathed hard, and a convulsive motion agitated its limbs."
- Mary Shelley, Frankenstein 


This technical paper, which was published in Wilmott September magazine, is the second in a series of two on the design of software systems in computational finance. We created multi-language application to value an Equity-linked product. The actual pricing of this product was performed by using QuantLib Monte Carlo framework. We used C++/CLI code as a wrapper class for native QuantLib C++ code. Then, we used C# as a front end to C++/CLI wrapper, after having constructed transaction-related parameters and market data. For flexible input data construction, a specific factory mechanism was implemented by using C# Assembly, Reflection API, and Dynamic data types. Finally, we interfaced C# client code to Excel by using Excel-DNA.

The entire source code for this application is presented in this blog post. Since the paper discussed how we implemented this application, we feel that being able to compile and run the actual code is necessary in order to understand the complete design. The original post for Equity-linked note implementation (including the original transaction term sheet) can be found in here.

The actual paper can be found in here. Thanks for reading this blog.
-Mike


C++ project


In Visual Studio, create a new C++/CLR Class Library project (QLWrapper).

At this point, pre-installed and pre-built QuantLib and Boost libraries should be available. In the case one may not have these libraries available, all required procedures for getting this part done correctly is well presented in here and here. Create references to required Boost and QuantLib header files and libraries as follows.









Next, some C++/CLI project settings needs to be modified. Disable the use of pre-compiled headers.









Update properties to suppress some specific warnings.








Optionally, update properties to suppress (almost) all the other warnings.









Next, add required C++ header and source files as presented below. Add new file for containing native C++ header file information (EquityLinkedNote.h).

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

namespace QuantLibCppNative {

 // instrument implementation for equity-linked note
 class EquityLinkedNote : public Instrument {
 public:
  // forward class declarations
  class arguments;
  class engine;

  // ctor and implementations for required base class methods
  EquityLinkedNote(Real notional, Real initialFixing, const std::vector<Date>& fixingDates,
   const std::vector<Date>& paymentDates, Real cap, Real floor);
  bool isExpired() const;

 private:
  void setupArguments(PricingEngine::arguments* args) const;
  // term sheet information
  Real notional_;
  Real initialFixing_;
  std::vector<Date> fixingDates_;
  std::vector<Date> paymentDates_;
  Real cap_;
  Real floor_;
 };

 // inner arguments class
 class EquityLinkedNote::arguments : public PricingEngine::arguments {
 public:
  void validate() const;
  Real notional;
  Real initialFixing;
  std::vector<Date> fixingDates;
  std::vector<Date> paymentDates;
  Real cap;
  Real floor;
 };

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

 // path pricer implementation for equity-linked note
 class EquityLinkedNotePathPricer : public PathPricer < Path > {
 public:
  EquityLinkedNotePathPricer(Real notional, Real initialFixing, const std::vector<Date>& fixingDates,
   const std::vector<Date>& paymentDates, Real cap, Real floor, const Handle<YieldTermStructure>& curve);
  Real operator()(const Path& path) const;
 private:
  Real notional_;
  Real initialFixing_;
  std::vector<Date> fixingDates_;
  std::vector<Date> paymentDates_;
  Real cap_;
  Real floor_;
  Handle<YieldTermStructure> curve_;
 };

 // monte carlo framework engine implementation for base engine class
 template <typename RNG = PseudoRandom, typename S = Statistics>
 class EquityLinkedNoteMonteCarloPricer : public EquityLinkedNote::engine, private McSimulation <SingleVariate, RNG, S> {
 public:
  // ctor
  EquityLinkedNoteMonteCarloPricer(const boost::shared_ptr<StochasticProcess>& process,
   const Handle<YieldTermStructure>& curve, bool antitheticVariate, Size requiredSamples,
   Real requiredTolerance, Size maxSamples, BigNatural seed)
   : McSimulation<SingleVariate, RNG, S>(antitheticVariate, false), process_(process), curve_(curve),
   requiredSamples_(requiredSamples), requiredTolerance_(requiredTolerance), maxSamples_(maxSamples), seed_(seed) {
   // register observer (pricer) with observables (stochastic process, curve)
   registerWith(process_);
   registerWith(curve_);
  }

  // implementation for required base engine class method
  void calculate() const {
   // the actual simulation process will be performed within the following method
   McSimulation<SingleVariate, RNG, S>::calculate(requiredTolerance_, requiredSamples_, maxSamples_);
   this->results_.value = this->mcModel_->sampleAccumulator().mean();
   //
   if (RNG::allowsErrorEstimate) {
    this->results_.errorEstimate = this->mcModel_->sampleAccumulator().errorEstimate();
   }
   else {
    this->results_.errorEstimate = Null<Real>();
   }
  }

 private:
  // type definitions
  typedef McSimulation<SingleVariate, RNG, S> simulation;
  typedef typename simulation::path_pricer_type path_pricer_type;
  typedef typename simulation::path_generator_type path_generator_type;

  // implementation for McSimulation class virtual method
  TimeGrid timeGrid() const {
   // create time grid based on a set of given index fixing dates
   Date referenceDate = curve_->referenceDate();
   DayCounter dayCounter = curve_->dayCounter();
   std::vector<Time> fixingTimes(arguments_.fixingDates.size());
   for (Size i = 0; i != fixingTimes.size(); ++i) {
    fixingTimes[i] = dayCounter.yearFraction(referenceDate, arguments_.fixingDates[i]);
   }
   return TimeGrid(fixingTimes.begin(), fixingTimes.end());
  }

  // implementation for McSimulation class virtual method
  boost::shared_ptr<path_generator_type> pathGenerator() const {
   // create time grid and get information concerning number of simulation steps for a path
   TimeGrid grid = timeGrid();
   Size steps = (grid.size() - 1);
   // create random sequence generator and return path generator
   typename RNG::rsg_type generator = RNG::make_sequence_generator(steps, seed_);
   return boost::make_shared<path_generator_type>(process_, grid, generator, false);
  }

  // implementation for McSimulation class virtual method
  boost::shared_ptr<path_pricer_type> pathPricer() const {
   // create path pricer implementation for equity-linked note
   return boost::make_shared<EquityLinkedNotePathPricer>(arguments_.notional, arguments_.initialFixing,
    arguments_.fixingDates, arguments_.paymentDates, arguments_.cap, arguments_.floor, this->curve_);
  }

  // private data members
  boost::shared_ptr<StochasticProcess> process_;
  Handle<YieldTermStructure> curve_;
  Size requiredSamples_;
  Real requiredTolerance_;
  Size maxSamples_;
  BigNatural seed_;
 };
}

Add new file for containing native C++ source file information (EquityLinkedNote.cpp).

// EquityLinkedNote.cpp
#include "EquityLinkedNote.h"
#include <algorithm>

namespace QuantLibCppNative {
 // implementations for equity-linked note methods
 EquityLinkedNote::EquityLinkedNote(Real notional, Real initialFixing, const std::vector<Date>& fixingDates,
  const std::vector<Date>& paymentDates, Real cap, Real floor)
  : notional_(notional), initialFixing_(initialFixing), fixingDates_(fixingDates),
  paymentDates_(paymentDates), cap_(cap), floor_(floor) {
  // ctor
 }

 bool EquityLinkedNote::isExpired() const {
  Date valuationDate = Settings::instance().evaluationDate();
  // note is expired, if valuation date is greater than the last fixing date
  if (valuationDate > fixingDates_.back())
   return true;
  return false;
 }

 void EquityLinkedNote::setupArguments(PricingEngine::arguments* args) const {
  EquityLinkedNote::arguments* args_ = dynamic_cast<EquityLinkedNote::arguments*>(args);
  QL_REQUIRE(args_ != nullptr, "arguments casting error");
  args_->notional = notional_;
  args_->initialFixing = initialFixing_;
  args_->fixingDates = fixingDates_;
  args_->paymentDates = paymentDates_;
  args_->cap = cap_;
  args_->floor = floor_;
 }

 void EquityLinkedNote::arguments::validate() const {
  // checks for some general argument properties
  QL_REQUIRE(notional > 0.0, "notional must be greater than zero");
  QL_REQUIRE(initialFixing > 0.0, "initial fixing must be greater than zero");
  QL_REQUIRE(cap > floor, "cap must be greater than floor");
  // check for date consistency : all payment dates have to be included 
  // within a set of given fixing dates
  for (int i = 0; i != paymentDates.size(); ++i) {
   if (std::find(fixingDates.begin(), fixingDates.end(), paymentDates[i]) == fixingDates.end()) {
    QL_REQUIRE(false, "payment date has to be included within given fixing dates");
   }
  }
 }

 // implementations for equity-linked path pricer methods
 EquityLinkedNotePathPricer::EquityLinkedNotePathPricer(Real notional, Real initialFixing, const std::vector<Date>& fixingDates,
  const std::vector<Date>& paymentDates, Real cap, Real floor, const Handle<YieldTermStructure>& curve)
  : notional_(notional), initialFixing_(initialFixing), fixingDates_(fixingDates),
  paymentDates_(paymentDates), cap_(cap), floor_(floor), curve_(curve) {
  // ctor
 }

 // the actual pricing algorithm for a simulated path is implemented in this method
 Real EquityLinkedNotePathPricer::operator()(const Path& path) const {
  Real coupon = 0.0;
  Real cumulativeCoupon = 0.0;
  Real aggregatePathPayoff = 0.0;
  int paymentDateCounter = 0;

  // loop through fixing dates
  for (int i = 1; i != fixingDates_.size(); ++i) {
   // calculate floating coupon, based on simulated index fixing values
   coupon = std::min(path.at(i) / path.at(i - 1) - 1, cap_);
   // add floating coupon to cumulative coupon
   cumulativeCoupon += coupon;

   // calculate period payoff for each payment date
   if (fixingDates_[i] == paymentDates_[paymentDateCounter]) {
    // calculate discounted payoff for current period, add value to aggregate path payoff
    aggregatePathPayoff += std::max(cumulativeCoupon, floor_) * notional_ * curve_->discount(fixingDates_[i]);
    // re-initialize cumulative coupon to zero, look for the next payment date
    cumulativeCoupon = 0.0;
    paymentDateCounter++;
   }
  }
  return aggregatePathPayoff;
 }
}

Update the existing file to contain managed C++ header file information (QLWrapper.h).

// QLWrapper.h
#pragma once
#include "EquityLinkedNote.h"
using namespace System;
using namespace System::Collections::Generic;

namespace QLWrapper {

 // C++/CLI wrapper class
 public ref class EquityLinkedNote {
 
 public:
  EquityLinkedNote(double notional, double cap, double floor, DateTime transactionDate, 
   int settlementDays, String^ calendar, String^ dayCountConvention, int requiredSamples, 
   int seed, List<DateTime>^ fixingDates, List<DateTime>^ paymentDates, 
   double initialFixing, double volatility, Dictionary<DateTime, double>^ curve);
  !EquityLinkedNote();
  ~EquityLinkedNote();
  double PV();
 
 private:
  // note : class members as native pointers
  QuantLibCppNative::EquityLinkedNote* note;
  QuantLibCppNative::EquityLinkedNoteMonteCarloPricer<PseudoRandom, Statistics>* pricer;
 };
}

namespace QuantLibConversions {
 
 // one static class for all QuantLib-related type conversions
 public ref class Convert abstract sealed {
 public:

  // convert System.String to QuantLib.DayCounter class
  static DayCounter ToDayCounter(String^ dayCounterString);

  // convert System.DateTime to QuantLib.Date class
  static Date ToDate(DateTime dateTime);

  // convert System.String to QuantLib.Calendar class
  static Calendar ToCalendar(String^ calendarString);

  // convert System.String to QuantLib.BusinessDayConvention enumerator
  static BusinessDayConvention ToBusinessDayConvention(String^ businessDayConventionString);

 };
}

Update the existing file to contain managed C++ source file information (QLWrapper.cpp).

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

namespace QLWrapper {
 using QL = QuantLibConversions::Convert;

 EquityLinkedNote::EquityLinkedNote(double notional_, double cap_, double floor_,
  DateTime transactionDate_, int settlementDays_, String^ calendar_,
  String^ dayCountConvention_, int requiredSamples_, int seed_,
  List<DateTime>^ fixingDates_, List<DateTime>^ paymentDates_,
  double initialFixing_, double volatility_, Dictionary<DateTime, double>^ curve_) {

  // create QL calendar from a given string
  Calendar calendar = QL::ToCalendar(calendar_);

  // create QL daycounter from a given string
  DayCounter dayCountConvention = QL::ToDayCounter(dayCountConvention_);

  Date transactionDate = QL::ToDate(transactionDate_);
  Date settlementDate = calendar.advance(transactionDate, Period(settlementDays_, Days));
  Settings::instance().evaluationDate() = settlementDate;

  // import fixing dates to std::vector
  std::vector<Date> fixingDates;
  for each (System::DateTime dt in fixingDates_) {
   fixingDates.push_back(QL::ToDate(dt));
  }

  // import payment dates to std::vector
  std::vector<Date> paymentDates;
  for each (System::DateTime dt in paymentDates_) {
   paymentDates.push_back(QL::ToDate(dt));
  }

  // create structured equity-linked note
  note = new QuantLibCppNative::EquityLinkedNote(notional_, initialFixing_, fixingDates, paymentDates, cap_, floor_);

  // create std::vector containers for dates and discount factors
  std::vector<Date> maturityDates;
  std::vector<double> discountFactors;
  for each (KeyValuePair<DateTime, double> kvp in curve_) {
   maturityDates.push_back(QL::ToDate(kvp.Key));
   discountFactors.push_back(kvp.Value);
  }

  // create valuation curve from a set of given discount factors
  auto riskFreeRateTermStructure = boost::make_shared<InterpolatedDiscountCurve<LogLinear>>
   (maturityDates, discountFactors, dayCountConvention, calendar);
  Handle<YieldTermStructure> riskFreeRateTermStructureHandle(riskFreeRateTermStructure);

  // create stochastic process
  Handle<Quote> initialFixingHandle(boost::shared_ptr<Quote>(new SimpleQuote(initialFixing_)));
  auto volatilityQuote = boost::make_shared<SimpleQuote>(volatility_);
  Handle<Quote> volatilityHandle(volatilityQuote);
  Handle<BlackVolTermStructure> volatilityTermStructureHandle(boost::shared_ptr<BlackVolTermStructure>
   (new BlackConstantVol(settlementDays_, calendar, volatilityHandle, dayCountConvention)));
  auto process = boost::make_shared<BlackScholesProcess>(initialFixingHandle, riskFreeRateTermStructureHandle, volatilityTermStructureHandle);

  // create pricer instance
  pricer = new QuantLibCppNative::EquityLinkedNoteMonteCarloPricer<PseudoRandom, Statistics>
   (process, riskFreeRateTermStructureHandle, false, static_cast<Size>(requiredSamples_),
   Null<Real>(), static_cast<Size>(requiredSamples_), static_cast<BigNatural>(seed_));

  // assign pricer to instrument
  // note : cast pricer from native pointer to boost shared pointer
  note->setPricingEngine(static_cast<boost::shared_ptr<
   QuantLibCppNative::EquityLinkedNoteMonteCarloPricer<PseudoRandom, Statistics>>>(pricer));
 }
 EquityLinkedNote::!EquityLinkedNote() {
  delete note;
  delete pricer;
 }
 EquityLinkedNote::~EquityLinkedNote() {
  this->!EquityLinkedNote();
 }
 double EquityLinkedNote::PV() {
  return note->NPV();
 }
}

namespace QuantLibConversions {

 DayCounter Convert::ToDayCounter(String^ dayCounterString) {
  if (dayCounterString->ToUpper() == "ACTUAL360") return Actual360();
  if (dayCounterString->ToUpper() == "THIRTY360") return Thirty360();
  if (dayCounterString->ToUpper() == "ACTUALACTUAL") return ActualActual();
  if (dayCounterString->ToUpper() == "BUSINESS252") return Business252();
  if (dayCounterString->ToUpper() == "ACTUAL365NOLEAP") return Actual365NoLeap();
  if (dayCounterString->ToUpper() == "ACTUAL365FIXED") return Actual365Fixed();
  // requested day counter not found, throw exception
  throw gcnew System::Exception("undefined daycounter");
 }

 Date Convert::ToDate(DateTime dateTime) {
  // Date constructor using Excel dateserial
  return Date(dateTime.ToOADate());
 }

 Calendar Convert::ToCalendar(String^ calendarString) {
  if (calendarString->ToUpper() == "ARGENTINA.MERVAL") return Argentina(Argentina::Market::Merval);
  if (calendarString->ToUpper() == "AUSTRALIA") return Australia();
  if (calendarString->ToUpper() == "BRAZIL.EXCHANGE") return Brazil(Brazil::Market::Exchange);
  if (calendarString->ToUpper() == "BRAZIL.SETTLEMENT") return Brazil(Brazil::Market::Settlement);
  if (calendarString->ToUpper() == "CANADA.SETTLEMENT") return Canada(Canada::Market::Settlement);
  if (calendarString->ToUpper() == "CANADA.TSX") return Canada(Canada::Market::TSX);
  if (calendarString->ToUpper() == "CHINA.IB") return China(China::Market::IB);
  if (calendarString->ToUpper() == "CHINA.SSE") return China(China::Market::SSE);
  if (calendarString->ToUpper() == "CZECHREPUBLIC.PSE") return CzechRepublic(CzechRepublic::Market::PSE);
  if (calendarString->ToUpper() == "DENMARK") return Denmark();
  if (calendarString->ToUpper() == "FINLAND") return Finland();
  if (calendarString->ToUpper() == "GERMANY.SETTLEMENT") return Germany(Germany::Market::Settlement);
  if (calendarString->ToUpper() == "GERMANY.FRANKFURTSTOCKEXCHANGE") return Germany(Germany::Market::FrankfurtStockExchange);
  if (calendarString->ToUpper() == "GERMANY.XETRA") return Germany(Germany::Market::Xetra);
  if (calendarString->ToUpper() == "GERMANY.EUREX") return Germany(Germany::Market::Eurex);
  if (calendarString->ToUpper() == "GERMANY.EUWAX") return Germany(Germany::Market::Euwax);
  if (calendarString->ToUpper() == "HONGKONG.HKEX") return HongKong(HongKong::Market::HKEx);
  if (calendarString->ToUpper() == "INDIA.NSE") return India(India::Market::NSE);
  if (calendarString->ToUpper() == "INDONESIA.BEJ") return Indonesia(Indonesia::Market::BEJ);
  if (calendarString->ToUpper() == "INDONESIA.IDX") return Indonesia(Indonesia::Market::IDX);
  if (calendarString->ToUpper() == "INDONESIA.JSX") return Indonesia(Indonesia::Market::JSX);
  if (calendarString->ToUpper() == "ISRAEL.SETTLEMENT") return Israel(Israel::Market::Settlement);
  if (calendarString->ToUpper() == "ISRAEL.TASE") return Israel(Israel::Market::TASE);
  if (calendarString->ToUpper() == "ITALY.EXCHANGE") return Italy(Italy::Market::Exchange);
  if (calendarString->ToUpper() == "ITALY.SETTLEMENT") return Italy(Italy::Market::Settlement);
  if (calendarString->ToUpper() == "JAPAN") return Japan();
  if (calendarString->ToUpper() == "MEXICO.BMV") return Mexico(Mexico::Market::BMV);
  if (calendarString->ToUpper() == "NEWZEALAND") return NewZealand();
  if (calendarString->ToUpper() == "NORWAY") return Norway();
  if (calendarString->ToUpper() == "POLAND") return Poland();
  if (calendarString->ToUpper() == "ROMANIA") return Romania();
  if (calendarString->ToUpper() == "RUSSIA.MOEX") return Russia(Russia::Market::MOEX);
  if (calendarString->ToUpper() == "RUSSIA.SETTLEMENT") return Russia(Russia::Market::Settlement);
  if (calendarString->ToUpper() == "SAUDIARABIA.TADAWUL") return SaudiArabia(SaudiArabia::Market::Tadawul);
  if (calendarString->ToUpper() == "SINGAPORE.SGX") return Singapore(Singapore::Market::SGX);
  if (calendarString->ToUpper() == "SLOVAKIA.BSSE") return Slovakia(Slovakia::Market::BSSE);
  if (calendarString->ToUpper() == "SOUTHAFRICA") return SouthAfrica();
  if (calendarString->ToUpper() == "SOUTHKOREA.KRX") return SouthKorea(SouthKorea::Market::KRX);
  if (calendarString->ToUpper() == "SOUTHKOREA.SETTLEMENT") return SouthKorea(SouthKorea::Market::Settlement);
  if (calendarString->ToUpper() == "SWEDEN") return Sweden();
  if (calendarString->ToUpper() == "SWITZERLAND") return Switzerland();
  if (calendarString->ToUpper() == "TAIWAN.TSEC") return Taiwan(Taiwan::Market::TSEC);
  if (calendarString->ToUpper() == "TARGET") return TARGET();
  if (calendarString->ToUpper() == "TURKEY") return Turkey();
  if (calendarString->ToUpper() == "UKRAINE.USE") return Ukraine(Ukraine::Market::USE);
  if (calendarString->ToUpper() == "UNITEDKINGDOM.EXCHANGE") return UnitedKingdom(UnitedKingdom::Market::Exchange);
  if (calendarString->ToUpper() == "UNITEDKINGDOM.METALS") return UnitedKingdom(UnitedKingdom::Market::Metals);
  if (calendarString->ToUpper() == "UNITEDKINGDOM.SETTLEMENT") return UnitedKingdom(UnitedKingdom::Market::Settlement);
  if (calendarString->ToUpper() == "UNITEDSTATES.GOVERNMENTBOND") return UnitedStates(UnitedStates::Market::GovernmentBond);
  if (calendarString->ToUpper() == "UNITEDSTATES.LIBORIMPACT") return UnitedStates(UnitedStates::Market::LiborImpact);
  if (calendarString->ToUpper() == "UNITEDSTATES.NERC") return UnitedStates(UnitedStates::Market::NERC);
  if (calendarString->ToUpper() == "UNITEDSTATES.NYSE") return UnitedStates(UnitedStates::Market::NYSE);
  if (calendarString->ToUpper() == "UNITEDSTATES.SETTLEMENT") return UnitedStates(UnitedStates::Market::Settlement);
  // requested calendar not found, throw exception
  throw gcnew System::Exception("undefined calendar");
 }

 BusinessDayConvention Convert::ToBusinessDayConvention(String^ businessDayConventionString) {
  if (businessDayConventionString->ToUpper() == "FOLLOWING") return BusinessDayConvention::Following;
  if (businessDayConventionString->ToUpper() == "HALFMONTHMODIFIEDFOLLOWING") return BusinessDayConvention::HalfMonthModifiedFollowing;
  if (businessDayConventionString->ToUpper() == "MODIFIEDFOLLOWING") return BusinessDayConvention::ModifiedFollowing;
  if (businessDayConventionString->ToUpper() == "MODIFIEDPRECEDING") return BusinessDayConvention::ModifiedPreceding;
  if (businessDayConventionString->ToUpper() == "NEAREST") return BusinessDayConvention::Nearest;
  if (businessDayConventionString->ToUpper() == "PRECEDING") return BusinessDayConvention::Preceding;
  if (businessDayConventionString->ToUpper() == "UNADJUSTED") return BusinessDayConvention::Unadjusted;
  // requested business day convention not found, throw exception
  throw gcnew System::Exception("undefined business day convention");
 }
}

After this, one should be able to build this C++ project successfully.

C# project


In the first stage, a simple console application is implemented, in order to quickly test the core C++ program from C# client program. Add new C# console project (CsClient). Then, add file containing C# tester program.

using System;
using System.Collections.Generic;

namespace CsClient {
    class Program {
        static void Main(string[] args) {
            try {
                double notional = 1000000.0;
                double initialFixing = 3662.18;
                double volatility = 0.16;
                double cap = 0.015;
                double floor = 0.0;
                DateTime transactionDate = new DateTime(2017, 10, 30);
                int settlementDays = 2;
                string calendar = "TARGET";
                string dayCountConvention = "ACTUAL360";
                int requiredSamples = 1000;
                int seed = 0;
                List<DateTime> fixingDates = new List<DateTime>() {
                    new DateTime(2017, 11, 30), new DateTime(2017, 12, 30), new DateTime(2018, 1, 30), 
                    new DateTime(2018, 2, 28), new DateTime(2018, 3, 30), new DateTime(2018, 4, 30), 
                    new DateTime(2018, 5, 30), new DateTime(2018, 6, 30), new DateTime(2018, 7, 30), 
                    new DateTime(2018, 8, 30), new DateTime(2018, 9, 30), new DateTime(2018, 10, 30), 
                    new DateTime(2018, 11, 30), new DateTime(2018, 12, 30), new DateTime(2019, 1, 30), 
                    new DateTime(2019, 2, 28), new DateTime(2019, 3, 30), new DateTime(2019, 4, 30), 
                    new DateTime(2019, 5, 30), new DateTime(2019, 6, 30), new DateTime(2019, 7, 30), 
                    new DateTime(2019, 8, 30), new DateTime(2019, 9, 30), new DateTime(2019, 10, 30), 
                    new DateTime(2019, 11, 30), new DateTime(2019, 12, 30), new DateTime(2020, 1, 30), 
                    new DateTime(2020, 2, 29), new DateTime(2020, 3, 30), new DateTime(2020, 4, 30), 
                    new DateTime(2020, 5, 30), new DateTime(2020, 6, 30), new DateTime(2020, 7, 30), 
                    new DateTime(2020, 8, 30), new DateTime(2020, 9, 30), new DateTime(2020, 10, 30)
                };
                List<DateTime> paymentDates = new List<DateTime>() {
                    new DateTime(2018, 10, 30), new DateTime(2019, 10, 30), new DateTime(2020, 10, 30)
                };
                Dictionary<DateTime, double> curve = new Dictionary<DateTime, double>() {
                    { new DateTime(2017, 10, 30), 1.0 }, { new DateTime(2018, 10, 30), 0.98 },
                    { new DateTime(2019, 10, 30), 0.96 }, { new DateTime(2020, 10, 30), 0.92 },
                    { new DateTime(2021, 10, 30), 0.85 }
                };
                QLWrapper.EquityLinkedNote wrapperNote = new QLWrapper.EquityLinkedNote(notional, cap, floor, transactionDate, settlementDays,
                    calendar, dayCountConvention, requiredSamples, seed, fixingDates, paymentDates, initialFixing, volatility, curve);
                double pv = wrapperNote.PV();
                Console.WriteLine(pv.ToString());
                GC.SuppressFinalize(wrapperNote);
            }
            catch (Exception e) {
                Console.WriteLine(e.Message);
            }
        }
    }
}

Set C# project as StartUp project and add reference to C++ project (QLWrapper).

At the moment, C# console client is using managed C++ code, which is wrapping native C++ code, which uses QuantLib C++ library. At this point, build should be successfully completed and previous C# program should return PV for equity-linked note.

Configurations and Factories


In order to get rid of all hard-coded market data and transaction parameters in previous C# client, configurations and data factories will be implemented. Next, implement the following XML configuration file. Remember to define the correct paths for XML and Access files into this configuration file.

<configurations>
     <XMLFilePathName>..\EquityLinkedNote.xml</XMLFilePathName>
    <connectionString>Provider=Microsoft.ACE.OLEDB.12.0;Data Source=..\Market.accdb;Jet OLEDB:Database Password=MyDbPassword;</connectionString>
</configurations>

Then, implement the following XML presentation for Equity-linked note transaction.

<EquityLinkedNote>
  <notional>1000000.0</notional>
  <cap>0.015</cap>
  <floor>0.0</floor>
  <transactionDate>2017-10-30T00:00:00</transactionDate>
  <settlementDays>2</settlementDays>
  <calendar>TARGET</calendar>
  <dayCountConvention>ACTUAL360</dayCountConvention>
  <requiredSamples>1000</requiredSamples>
  <seed>0</seed>
  <fixingDates>
    <dateTime>2017-11-30T00:00:00</dateTime>    <dateTime>2017-12-30T00:00:00</dateTime>
    <dateTime>2018-01-30T00:00:00</dateTime>    <dateTime>2018-02-28T00:00:00</dateTime>
    <dateTime>2018-03-30T00:00:00</dateTime>    <dateTime>2018-04-30T00:00:00</dateTime>
    <dateTime>2018-05-30T00:00:00</dateTime>    <dateTime>2018-06-30T00:00:00</dateTime>
    <dateTime>2018-07-30T00:00:00</dateTime>    <dateTime>2018-08-30T00:00:00</dateTime>
    <dateTime>2018-09-30T00:00:00</dateTime>    <dateTime>2018-10-30T00:00:00</dateTime>
    <dateTime>2018-11-30T00:00:00</dateTime>    <dateTime>2018-12-30T00:00:00</dateTime>
    <dateTime>2019-01-30T00:00:00</dateTime>    <dateTime>2019-02-28T00:00:00</dateTime>
    <dateTime>2019-03-30T00:00:00</dateTime>    <dateTime>2019-04-30T00:00:00</dateTime>
    <dateTime>2019-05-30T00:00:00</dateTime>    <dateTime>2019-06-30T00:00:00</dateTime>
    <dateTime>2019-07-30T00:00:00</dateTime>    <dateTime>2019-08-30T00:00:00</dateTime>
    <dateTime>2019-09-30T00:00:00</dateTime>    <dateTime>2019-10-30T00:00:00</dateTime>
    <dateTime>2019-11-30T00:00:00</dateTime>    <dateTime>2019-12-30T00:00:00</dateTime>
    <dateTime>2020-01-30T00:00:00</dateTime>    <dateTime>2020-02-29T00:00:00</dateTime>
    <dateTime>2020-03-30T00:00:00</dateTime>    <dateTime>2020-04-30T00:00:00</dateTime>
    <dateTime>2020-05-30T00:00:00</dateTime>    <dateTime>2020-06-30T00:00:00</dateTime>
    <dateTime>2020-07-30T00:00:00</dateTime>    <dateTime>2020-08-30T00:00:00</dateTime>
    <dateTime>2020-09-30T00:00:00</dateTime>    <dateTime>2020-10-30T00:00:00</dateTime>
  </fixingDates>
  <paymentDates>
    <dateTime>2018-10-30T00:00:00</dateTime>
    <dateTime>2019-10-30T00:00:00</dateTime>
    <dateTime>2020-10-30T00:00:00</dateTime>
  </paymentDates>
</EquityLinkedNote>

Next, prepare MS Access database (name: Market.accdb) for containing all required market information. For the reasons of convenience we use simple MS Access database to host market data. However, extending this code for more realistic settings (for example, SQL Server) should not pose difficulties for an experienced developer.











Add C# file containing abstraction for EquityLinkedNote and required factories for XML transaction and market data.

// EquityLinkedNote.cs
using System;
using System.Collections.Generic;
using System.Xml.Serialization;
using System.IO;
using System.Data;
using System.Data.OleDb;

namespace CsClient
{
    // interface for all factory classes
    public interface IFactory {
        // return any type
        dynamic Create();
    }

    // XML factory : create equity-linked note from XML file
    public class XMLNoteFactory : IFactory {
        public XMLNoteFactory() {
            // default ctor 
        }
        public dynamic Create() {
            
            // de-serialize equity-linked note object from XML file
            XmlSerializer serializer = new XmlSerializer(typeof(EquityLinkedNote));
            string XMLFilePathName = @CsClient.configurations.SelectSingleNode("configurations/XMLFilePathName").InnerText;
            EquityLinkedNote note = null;
            FileStream stream = File.OpenRead(XMLFilePathName);
            note = (EquityLinkedNote)serializer.Deserialize(stream);
            return note;
        }
    }

    // SQL database factory : create all required market data from database tables
    public class SQLMarketFactory : IFactory {
        public SQLMarketFactory() {
            // default ctor 
        }
        public dynamic Create() {

            // create connection string and SQL queries
            string connectionString = @CsClient.configurations.SelectSingleNode("configurations/connectionString").InnerText;

            // create and open connection
            OleDbConnection connection = new OleDbConnection(connectionString);
            connection.Open();

            // create adapter, request curve data
            OleDbDataAdapter adapter = new OleDbDataAdapter("select * from Curve", connection);
            DataSet dataset = new DataSet();
            adapter.Fill(dataset, "curve");

            // update command, request volatility
            adapter.SelectCommand.CommandText = "select * from Volatility";
            adapter.Fill(dataset, "volatility");

            // update command, request index fixing
            adapter.SelectCommand.CommandText = "select * from Fixings";
            adapter.Fill(dataset, "initialFixing");

            // extract data to curve dictionary
            Dictionary<DateTime, double> curve = new Dictionary<DateTime, double>();
            for (int i = 0; i < dataset.Tables["Curve"].Rows.Count; i++) {
                curve.Add(
                    dataset.Tables["curve"].Rows[i].Field<DateTime>("MaturityDate"), 
                    dataset.Tables["curve"].Rows[i].Field<double>("DiscountFactor"));
            }

            // extract volatility and initial fixing
            double volatility = dataset.Tables["volatility"].Rows[0].Field<double>("Rate");
            double initialFixing = dataset.Tables["initialFixing"].Rows[0].Field<double>("Rate");

            // pack all market data into tuple
            Tuple<Dictionary<DateTime, double>, double, double> market = 
                new Tuple<Dictionary<DateTime,double>,double,double>(curve, volatility, initialFixing);

            // dispose adapter, close connection, return tuple containing market
            adapter.Dispose();
            connection.Close();
            return market;
        }
    }

    // Equity-linked note class : container class for term sheet parameters
    public class EquityLinkedNote {

        public double notional;
        public double cap;
        public double floor;
        public DateTime transactionDate;
        public int settlementDays;
        public string calendar;
        public string dayCountConvention;
        public int requiredSamples;
        public int seed;
        public List<DateTime> fixingDates = null;
        public List<DateTime> paymentDates = null;

        EquityLinkedNote() {
            // default ctor required for XML de-serialization
        }

        public EquityLinkedNote(double notional_, double cap_, double floor_, DateTime transactionDate_, 
            int settlementDays_, string calendar_, string dayCountConvention_, int requiredSamples_, 
            int seed_, List<DateTime> fixingDates_, List<DateTime> paymentDates_) {
            
            notional = notional_;
            cap = cap_;
            floor = floor_;
            transactionDate = transactionDate_;
            settlementDays = settlementDays_;
            calendar = calendar_;
            dayCountConvention = dayCountConvention_;
            requiredSamples = requiredSamples_;
            seed = seed_;
            fixingDates = fixingDates_;
            paymentDates = paymentDates_;
        }
    }
}

Finally, update C# client for creating transaction parameters and market data from configured XML files. Remember to update path to configuration XML file.

// Program.cs
using System;
using System.Collections.Generic;
using System.Xml;

namespace CsClient {
    public static class CsClient {
        public static XmlDocument configurations = null;
        private static string configurationFilePath =
            @"..\configurations.xml";
        private static double result = 0.0;

        public static void Main() {
            try {
                // create configurations
                configurations = new XmlDocument();
                configurations.Load(configurationFilePath);

                // create and use factory to create equity-linked note object from XML file
                IFactory noteFactory = new XMLNoteFactory();
                EquityLinkedNote note = noteFactory.Create();

                // use factory to create market data from database
                IFactory marketDataFactory = new SQLMarketFactory();
                Tuple<Dictionary<DateTime, double>, double, double> market = marketDataFactory.Create();
                Dictionary<DateTime, double> curveData = market.Item1;
                double volatility = market.Item2;
                double initialFixing = market.Item3;

                // use C++/CLI wrapper : create equity-linked note instance
                QLWrapper.EquityLinkedNote wrapperNote =
                    new QLWrapper.EquityLinkedNote(note.notional, note.cap, note.floor,
                        note.transactionDate, note.settlementDays, note.calendar, note.dayCountConvention,
                        note.requiredSamples, note.seed, note.fixingDates, note.paymentDates, initialFixing, volatility, curveData);

                // use C++/CLI wrapper : value equity-linked note
                result = wrapperNote.PV();
                Console.WriteLine(result.ToString());
                GC.SuppressFinalize(wrapperNote);
            }
            catch (Exception e) {
                Console.WriteLine(e.Message);
            }
        }
    }
}

At this point, build should be successfully completed and our new C# client program should return PV for equity-linked note. All transaction parameters and required market data can now be modified, without touching the actual program.

Excel interfacing and Factory of Factories


We will be using Excel as input-output platform for our C# client program. For interfacing task we will use Excel-DNA. Dedicated blog post on how to implement this, can be found in here. However, for the sake of completeness, the whole procedure is also explained below on a detailed level.

First, prepare Excel GUI carefully, by following detailed instructions (range names) given in the screenshot below. For flexible transaction data construction, a specific factory mechanism is implemented by using C# Assembly, Reflection API, and Dynamic data types. Note, that the actual factory is created inside C# program, based on the string given in cell D2 (namespace.class: CsClient.XMLNoteFactory or CsClient.ExcelNoteFactory).

















Add reference to Excel-DNA library (Project - Add reference - Browse - \\ExcelDna.Integration.dll) and click OK. This dll file is inside the distribution folder what has been downloaded from Excel-DNA website. From the properties of this reference, set Copy Local to be False.

Add new file as text file to project (Project - Add new item - Text file) and name it to be CsClient.dna. CopyPaste the following xml code into this file.

<DnaLibrary Name="CsClient" RuntimeVersion="v4.0">
  <ExternalLibrary Path="CsClient.dll" />
</DnaLibrary>

From the properties of this dna file, set Copy to Output Directory to be Copy if newer.

Next, from the downloaded Excel-DNA folder (Distribution), copy ExcelDna.xll file into your project folder and rename it to be CsClient.xll. Then, add this xll file into your current project (Project - Add existing item). At this point, it might be that you do not see anything else, except cs files on this window. From drop down box on the bottom right corner of this window, select All files and you should see ExcelInterface.xll file what we just pasted into this ExcelInterface folder. Select this file and press Add. Finally, from the properties of this xll file, set Copy to Output Directory to be Copy if newer. At this point, we are done with C# and Excel-DNA.

Next, update EquityLinkedNote file for containing Excel factory for Equity-linked note.

// EquityLinkedNote.cs
using System;
using System.Collections.Generic;
using System.Xml.Serialization;
using System.IO;
using System.Data;
using System.Data.OleDb;
using ExcelDna.Integration;

namespace CsClient {
    // interface for all factory classes
    public interface IFactory {
        // return any type
        dynamic Create();
    }

    // Excel factory : create equity-linked note from Excel named ranges
    public class ExcelNoteFactory : IFactory {
        public ExcelNoteFactory() {
            // default ctor 
        }
        public dynamic Create() {

            // create Excel application
            dynamic Excel = ExcelDnaUtil.Application;

            // create all scalar parameters
            double notional = (double)Excel.Range["notional"].Value2;
            double cap = (double)Excel.Range["cap"].Value2;
            double floor = (double)Excel.Range["floor"].Value2;
            DateTime transactionDate = DateTime.FromOADate((double)Excel.Range["transactionDate"].Value2);
            int settlementDays = (int)Excel.Range["settlementDays"].Value2;
            string calendar = (string)Excel.Range["calendar"].Value2;
            string dayCountConvention = (string)Excel.Range["dayCountConvention"].Value2;
            int requiredSamples = (int)Excel.Range["requiredSamples"].Value2;
            int seed = (int)Excel.Range["seed"].Value2;

            // create list of fixing dates
            List<DateTime> fixingDates = new List<DateTime>();
            dynamic fixingDatesArray = Excel.Range["fixingDates"].Value2;
            for (int i = 0; i != fixingDatesArray.GetUpperBound(0); ++i) {
                fixingDates.Add(DateTime.FromOADate(Convert.ToDouble(fixingDatesArray.GetValue(i + 1, 1))));
            }

            // create list of payment dates
            List<DateTime> paymentDates = new List<DateTime>();
            dynamic paymentDatesArray = Excel.Range["paymentDates"].Value2;
            for (int i = 0; i != paymentDatesArray.GetUpperBound(0); ++i) {
                paymentDates.Add(DateTime.FromOADate(Convert.ToDouble(paymentDatesArray.GetValue(i + 1, 1))));
            }

            // create equity-linked note instance
            return new EquityLinkedNote(notional, cap, floor, transactionDate, settlementDays,
                calendar, dayCountConvention, requiredSamples, seed, fixingDates, paymentDates);
        }
    }

    // XML factory : create equity-linked note from XML file
    public class XMLNoteFactory : IFactory {
        public XMLNoteFactory() {
            // default ctor 
        }
        public dynamic Create() {

            // de-serialize equity-linked note object from XML file
            XmlSerializer serializer = new XmlSerializer(typeof(EquityLinkedNote));
            string XMLFilePathName = @CsClient.configurations.SelectSingleNode("configurations/XMLFilePathName").InnerText;
            EquityLinkedNote note = null;
            FileStream stream = File.OpenRead(XMLFilePathName);
            note = (EquityLinkedNote)serializer.Deserialize(stream);
            return note;
        }
    }

    // SQL database factory : create all required market data from database tables
    public class SQLMarketFactory : IFactory {
        public SQLMarketFactory() {
            // default ctor 
        }
        public dynamic Create() {

            // create connection string and SQL queries
            string connectionString = @CsClient.configurations.SelectSingleNode("configurations/connectionString").InnerText;

            // create and open connection
            OleDbConnection connection = new OleDbConnection(connectionString);
            connection.Open();

            // create adapter, request curve data
            OleDbDataAdapter adapter = new OleDbDataAdapter("select * from Curve", connection);
            DataSet dataset = new DataSet();
            adapter.Fill(dataset, "curve");

            // update command, request volatility
            adapter.SelectCommand.CommandText = "select * from Volatility";
            adapter.Fill(dataset, "volatility");

            // update command, request index fixing
            adapter.SelectCommand.CommandText = "select * from Fixings";
            adapter.Fill(dataset, "initialFixing");

            // extract data to curve dictionary
            Dictionary<DateTime, double> curve = new Dictionary<DateTime, double>();
            for (int i = 0; i < dataset.Tables["Curve"].Rows.Count; i++) {
                curve.Add(
                    dataset.Tables["curve"].Rows[i].Field<DateTime>("MaturityDate"),
                    dataset.Tables["curve"].Rows[i].Field<double>("DiscountFactor"));
            }

            // extract volatility and initial fixing
            double volatility = dataset.Tables["volatility"].Rows[0].Field<double>("Rate");
            double initialFixing = dataset.Tables["initialFixing"].Rows[0].Field<double>("Rate");

            // pack all market data into tuple
            Tuple<Dictionary<DateTime, double>, double, double> market =
                new Tuple<Dictionary<DateTime, double>, double, double>(curve, volatility, initialFixing);

            // dispose adapter, close connection, return tuple containing market
            adapter.Dispose();
            connection.Close();
            return market;
        }
    }

    // Equity-linked note class : container class for term sheet parameters
    public class EquityLinkedNote {

        public double notional;
        public double cap;
        public double floor;
        public DateTime transactionDate;
        public int settlementDays;
        public string calendar;
        public string dayCountConvention;
        public int requiredSamples;
        public int seed;
        public List<DateTime> fixingDates = null;
        public List<DateTime> paymentDates = null;

        EquityLinkedNote() {
            // default ctor required for XML de-serialization
        }

        public EquityLinkedNote(double notional_, double cap_, double floor_, DateTime transactionDate_,
            int settlementDays_, string calendar_, string dayCountConvention_, int requiredSamples_,
            int seed_, List<DateTime> fixingDates_, List<DateTime> paymentDates_) {

            notional = notional_;
            cap = cap_;
            floor = floor_;
            transactionDate = transactionDate_;
            settlementDays = settlementDays_;
            calendar = calendar_;
            dayCountConvention = dayCountConvention_;
            requiredSamples = requiredSamples_;
            seed = seed_;
            fixingDates = fixingDates_;
            paymentDates = paymentDates_;
        }
    }
}

Then, update C# client program for consisting interface to Excel and factory of factories. Since we might need to use Windows Forms object in our program (MessageBox in Catch block), we need to create reference to System.Windows.Forms library (Project - Add reference - Assemblies - System.Windows.Forms).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Windows.Forms;
using System.Xml;
using ExcelDna.Integration;

namespace CsClient {
    public static class CsClient {
        public static XmlDocument configurations = null;
        private static string configurationFilePath =
            @"..\configurations.xml";
        private static double result = 0.0;

        public static void Run() {
            try {
                // create Excel
                dynamic Excel = ExcelDnaUtil.Application;

                // create configurations
                configurations = new XmlDocument();
                configurations.Load(configurationFilePath);

                // get factory selection from Excel
                string assemblyFile = @configurations.SelectSingleNode("configurations/assemblyFilePathName").InnerText;
                string factorySelection = Excel.Range["factorySelection"].Value2;

                // create and use factory to create equity-linked note object
                dynamic noteFactory = LoadFactory(assemblyFile, factorySelection);
                dynamic note = noteFactory.Create();

                // use factory to create market data from database
                IFactory marketDataFactory = new SQLMarketFactory();
                Tuple<Dictionary<DateTime, double>, double, double> market = marketDataFactory.Create();
                Dictionary<DateTime, double> curveData = market.Item1;
                double volatility = market.Item2;
                double initialFixing = market.Item3;

                // use C++/CLI wrapper : create equity-linked note instance
                QLWrapper.EquityLinkedNote wrapperNote =
                    new QLWrapper.EquityLinkedNote(note.notional, note.cap, note.floor,
                        note.transactionDate, note.settlementDays, note.calendar,
                        note.dayCountConvention, note.requiredSamples, note.seed,
                        note.fixingDates, note.paymentDates, initialFixing, volatility, curveData);

                // use C++/CLI wrapper : value equity-linked note
                result = wrapperNote.PV();
                Excel.Range["pv"] = result;
                GC.SuppressFinalize(wrapperNote);
            }
            catch (Exception e) {
                MessageBox.Show(e.Message);
            }
        }

        // Load requested factory from a given assembly
        public static dynamic LoadFactory(string assemblyFile, string factoryFullName) {
            dynamic factory = null;
            Assembly assembly = Assembly.LoadFrom(assemblyFile);
            Type[] types = assembly.GetTypes();
            foreach (Type type in types) {
                if (type.FullName == factoryFullName) {
                    factory = assembly.CreateInstance(type.FullName);
                    break;
                }
            }
            return factory;
        }

    }
}

Next, update XML configuration file with path name to dll file, which is containing C# project assembly.

<configurations>
    <assemblyFilePathName>..\CsClient.dll</assemblyFilePathName>
    <XMLFilePathName>..\QLWrapper\EquityLinkedNote.xml</XMLFilePathName>
    <connectionString>Provider=Microsoft.ACE.OLEDB.12.0;Data Source=..\Market.accdb;Jet OLEDB:Database Password=MyDbPassword;</connectionString>
</configurations>

Finally, change C# project type to Class Library (Project - CsClient Properties - Application - Output Type - Class Library). At this point we are done with the program part. Open the actual Excel interface workbook, which has been created earlier. Open created CsClient.xll file in the open Excel workbook. For existing action button found in the worksheet, assign macro (name: Run) which will trigger C# program execution.

In this final application, Excel is using external data sources (MS Access, XML) and C# class library, which is using managed C++ code, which is wrapping native C++ code, which uses QuantLib C++ library.

Thanks for reading this blog.
-Mike