Showing posts with label Structured Products. Show all posts
Showing posts with label Structured Products. Show all posts

Thursday, April 13, 2023

QuantLib-Python: Monte Carlo Valuation for Power Reverse Dual-Currency Note (PRDC)

PRDC note


Essentially, PRDC note can be thought as taking a leveraged position on FX forward curve. Floating coupon rate is a function of FX rates, usually defined as follows.

Additional FX term (at t=0) will scale FX rate used in coupon and is usually set to be equal to FX forward at inception. Mathematically, this changes the slope of coupon payoff and therefore, adding desired leverage to the product.

Model for FX stochastics


From pricing perspective, we need to get a view on FX forward curve. This can be simulated by using Garman-KohlHagen stochastic process for an exchange rate, which is available in QuantLib-Python library. In essence, this process is Geometric Brownian Motion, with an added term for foreign interest rate. We will use this model, even it does not necessarily fully reflect the statistical properties of exchange rate.

Applying Ito's rule to Geometric Brownian Motion, gives us the following solution for an exchange rate S observed at time t.


Python program


In order to implement completely new instrument and corresponding new pricing engine in QuantLib, one should first implement these in QuantLib C++ library by following carefully the designed Instrument-Engine scheme and then, interface these newly created C++ types back to Python by using swig. However in here, we will skip the previous approach completely. Instead, we just take "all the useful bits and pieces" available in QuantLib-Python library and use these to implement our own valuation code.

QuantLib-Python object building documentation by David Duarte is available in here.

The complete code is available in here.

Libraries


The following two libraries are being used.

import numpy as np
import QuantLib as ql

FX path generator


Path generator implements spot rate simulation, based on a given 1D stochastic process. Generated path is exactly in line with the dates given in coupon schedule. Note, that the exact number of paths to be generated has not been defined in this class. Whoever will be the client for this class, can request as many paths as desired. 

class PathGenerator:
    def __init__(self, valuation_date, coupon_schedule, day_counter, process):        
        self.valuation_date = valuation_date
        self.coupon_schedule = coupon_schedule
        self.day_counter = day_counter
        self.process = process
        #
        self.create_time_grids()
        
    def create_time_grids(self):
        all_coupon_dates = np.array(self.coupon_schedule)
        remaining_coupon_dates = all_coupon_dates[all_coupon_dates > self.valuation_date]
        self.time_grid = np.array([self.day_counter.yearFraction(self.valuation_date, date) 
            for date in remaining_coupon_dates])
        self.time_grid = np.concatenate((np.array([0.0]), self.time_grid))
        self.grid_steps = np.diff(self.time_grid)
        self.n_steps = self.grid_steps.shape[0]
        
    def next_path(self):
        e = np.random.normal(0.0, 1.0, self.n_steps)
        spot = self.process.x0()
        dw = e * self.grid_steps
        path = np.zeros(self.n_steps, dtype=float)
        
        for i in range(self.n_steps):
            dt = self.grid_steps[i]
            t = self.time_grid[i]
            spot = self.process.evolve(t, spot, dt, dw[i])
            path[i] = spot

        return path


Monte Carlo pricer


Monte Carlo pricer class will use path generator for simulating requested number of FX paths. A call for method npv will trigger coupon rates simulation. Simulated rates will then be corrected by taking into account of intro coupon. Finally, a series of cash flows will be created, based on calculated coupon rates and schedule dates.

class MonteCarloPricerPRDC:
    def __init__(self, valuation_date, coupon_schedule, day_counter, notional, discount_curve_handle, 
        payoff_function, fx_path_generator, n_paths, intro_coupon_schedule, intro_coupon_rate):

        self.valuation_date = valuation_date
        self.coupon_schedule = coupon_schedule
        self.day_counter = day_counter
        self.notional = notional
        self.discount_curve_handle = discount_curve_handle
        self.payoff_function = payoff_function
        self.fx_path_generator = fx_path_generator
        self.n_paths = n_paths
        self.intro_coupon_schedule = intro_coupon_schedule
        self.intro_coupon_rate = intro_coupon_rate
        #
        self.create_coupon_dates()
        
    def create_coupon_dates(self):
        self.all_coupon_dates = np.array(self.coupon_schedule)
        self.past_coupon_dates = self.all_coupon_dates[self.all_coupon_dates < self.valuation_date]
        n_past_coupon_dates = self.past_coupon_dates.shape[0] - 1
        self.past_coupon_rates = np.full(n_past_coupon_dates, 0.0, dtype=float)        
        self.remaining_coupon_dates = self.all_coupon_dates[self.all_coupon_dates > self.valuation_date]
        self.time_grid = np.array([self.day_counter.yearFraction(self.valuation_date, date) 
            for date in self.remaining_coupon_dates])
        self.grid_steps = np.concatenate((np.array([self.time_grid[0]]), np.diff(self.time_grid)))
        self.n_steps = self.grid_steps.shape[0]
        
        if(self.intro_coupon_schedule==None):
            self.has_intro_coupon = False
        else:
            self.intro_coupon_dates = np.array(self.intro_coupon_schedule)
            self.remaining_intro_coupon_dates = self.intro_coupon_dates[self.intro_coupon_dates > self.valuation_date]
            self.n_remaining_intro_coupon_dates = self.remaining_intro_coupon_dates.shape[0]
            if(self.n_remaining_intro_coupon_dates > 0):
                self.has_intro_coupon = True
            else:
                self.has_intro_coupon = False

    def simulate_coupon_rates(self):
        self.simulated_coupon_rates = np.zeros(self.n_steps, dtype=float)
        
        for i in range(self.n_paths):
            path = self.fx_path_generator.next_path()
            for j in range(self.n_steps):
                self.simulated_coupon_rates[j] += self.payoff_function(path[j])

        self.simulated_coupon_rates = self.simulated_coupon_rates / self.n_paths        
        if(self.has_intro_coupon): self.append_intro_coupon_rates()
        self.coupon_rates = np.concatenate((self.past_coupon_rates, self.simulated_coupon_rates))
        self.n_coupon_cash_flows = self.coupon_rates.shape[0]

    def append_intro_coupon_rates(self):
        for i in range(self.n_remaining_intro_coupon_dates):
            self.simulated_coupon_rates[i] = self.intro_coupon_rate
    
    def create_cash_flows(self):        
        self.coupon_cash_flows = np.empty(self.n_coupon_cash_flows, dtype=ql.FixedRateCoupon)
        
        for i in range(self.n_coupon_cash_flows):
            self.coupon_cash_flows[i] = ql.FixedRateCoupon(self.all_coupon_dates[i+1], self.notional, 
                self.coupon_rates[i], self.day_counter, self.all_coupon_dates[i], self.all_coupon_dates[i+1])
        
        self.coupon_leg = ql.Leg(self.coupon_cash_flows)
        redemption = ql.Redemption(self.notional, self.all_coupon_dates[-1])
        self.redemption_leg = ql.Leg(np.array([redemption]))

    def npv(self):
        self.simulate_coupon_rates()
        self.create_cash_flows()
        
        self.redemption_leg_npv = ql.CashFlows.npv(self.redemption_leg, self.discount_curve_handle, False)
        self.coupon_leg_npv = ql.CashFlows.npv(self.coupon_leg, self.discount_curve_handle, False)
        
        date = [payment.date() for payment in self.coupon_leg]
        amount = [payment.amount() for payment in self.coupon_leg]
        amount[-1] += self.notional
        pv = [ql.CashFlows.npv(np.array([payment]), self.discount_curve_handle, False) for payment in self.coupon_leg]        
        pv[-1] += self.redemption_leg_npv
        
        self.cash_flow_table = np.array([date, amount, pv])
        return self.coupon_leg_npv + self.redemption_leg_npv    


Hard-coded factory


The creation of Garman-Kohlhagen process is isolated into hard-coded factory method. In here, we just use flat forward curves and non-calibrated short rate models for the sake of simplicity. For more realistic creation of the curves and model calibrations, check out this and this.

def process_factory():
    today = ql.Settings.instance().evaluationDate
    
    domestic_curve = ql.FlatForward(today, ql.QuoteHandle(ql.SimpleQuote(0.01)), ql.Actual360())
    domestic_curve = ql.YieldTermStructureHandle(domestic_curve)
    foreign_curve = ql.FlatForward(today, ql.QuoteHandle(ql.SimpleQuote(0.03)), ql.Actual360())
    foreign_curve = ql.YieldTermStructureHandle(foreign_curve)
    
    fx_vol = ql.QuoteHandle(ql.SimpleQuote(0.1))
    fx_vol_curve = ql.BlackVolTermStructureHandle(ql.BlackConstantVol(today, ql.NullCalendar(), fx_vol, ql.Actual360()))    
    fx_spot = ql.QuoteHandle(ql.SimpleQuote(133.2681))
    
    return ql.GarmanKohlagenProcess(fx_spot, foreign_curve, domestic_curve, fx_vol_curve)


Main program


Main program creates all required components (JPY discount curve, process, schedules, generator and pricer) and then requests npv and cash flow table as Numpy ndarray (indexing: [0] = coupon dates, [1] = cash flows, [2] = present values of cash flows).

today = ql.Date(11, 4, 2023)
ql.Settings.instance().evaluationDate = today

# create JPY discount curve
discount_curve = ql.FlatForward(today, ql.QuoteHandle(ql.SimpleQuote(0.005)), ql.Actual360())
discount_curve_handle = ql.YieldTermStructureHandle(discount_curve)

# create FX process
process = process_factory()

# create schedules for coupon- and intro coupon payments
effectiveDate = ql.Date(3, ql.September, 2015)
terminationDate = ql.Date(3, ql.September, 2041)
coupon_schedule = ql.MakeSchedule(effectiveDate, terminationDate, ql.Period(6, ql.Months), 
    backwards=True, calendar=ql.TARGET(), convention=ql.ModifiedFollowing)

intro_coupon_termination_date = ql.Date(3, ql.September, 2016)
intro_coupon_schedule = ql.MakeSchedule(effectiveDate, intro_coupon_termination_date, ql.Period(6, ql.Months), 
    backwards=True, calendar=ql.TARGET(), convention=ql.ModifiedFollowing)

# create FX path generator
fx_path_generator = PathGenerator(today, coupon_schedule, ql.Actual360(), process)

# create PRDC pricer
notional = 300000000.0
intro_coupon_rate = 0.022
n_paths = 10000
prdc_payoff_function = lambda fx_rate : min(max(0.122 * (fx_rate / 120.0) - 0.1, 0.0), 0.022)
prdc_pricer = MonteCarloPricerPRDC(ql.Settings.instance().evaluationDate, coupon_schedule, ql.Actual360(),notional, 
    discount_curve_handle, prdc_payoff_function, fx_path_generator, n_paths, intro_coupon_schedule, intro_coupon_rate)

# request results
npv_ccy = prdc_pricer.npv()
print('PV in CCY: {}'.format(npv_ccy))
jpy_eur = 145.3275
npv_eur = npv_ccy / jpy_eur
print('PV in EUR: {}'.format(npv_eur))
print()
print('Cash flow dates: {}'.format(prdc_pricer.cash_flow_table[0]))
print()
print('Cash flows: {}'.format(prdc_pricer.cash_flow_table[1]))
print()
print('Present values of cash flows: {}'.format(prdc_pricer.cash_flow_table[2]))

Thanks for reading this blog.
-Mike

Thursday, November 28, 2019

QuantLib-Python: Heston Monte Carlo Valuation for Autocallable Memory Coupon Note

In the spirit of the previous post, I was woodshedding an implementation for valuing Autocallable Memory Coupon note by using libraries available in QuantLib-Python. These products are embedding a series of out-of-the-money barrier options and for this specific reason, it is important to capture implied volatility smile by using appropriate model. For this implementation example, Heston stochastic volatility model has been used. In addition to the actual Monte Carlo algorithm and path generator, I also implemented a simple method for calibrating Heston model to volatility surface by using SciPy optimization package. The complete program can be downloaded from my GitHub page.

Autocallable Notes


Autocallable notes are path-dependent structured products, which may be linked to any index. However, this product is usually linked to equity index or equity basket. If condition called autocall barrier will be reached on any coupon date, the product will be terminated immediately, after paying back coupon (usually fixed) and notional amount. However, if the underlying index will not reach autocall barrier on a coupon date, but reaches condition called coupon barrier, only fixed coupon will be paid on notional and the product will be kept alive until maturity (or until the next autocall event). Autocallables may also have a memory, which means that if the underlying index was below coupon barrier in the previous coupon date (and there was no coupon payment), but will be above coupon barrier in the next coupon date, the product will pay all cumulated (and unpaid) coupons on notional in the next coupon date. Autocallable products are (usually) not capital protected, which means that the investor is not guaranteed to receive the initial investment. If the index fixing on a final coupon date will be below condition called protection barrier, the final redemption amount will be calculated according to specific formula and investor will lose some of the invested capital. There is an example brochure and term sheet available for this product in my GitHub page.

Library Imports


import QuantLib as ql
import numpy as np
import scipy.optimize as opt

Heston Path Generator


Below is a simple (hard-coded) method for generating paths by using Heston process for a given set of QuantLib dates, which can be unevenly distributed. A couple of notes about Heston path generation process in general. QuantLib MultiPathGenerator has to be used for simulating paths for this specific process, because "under the hood", QuantLib simulates two correlated random variates: one for the asset and another for the volatility. As a result for "one simulation round", MultiPath object will be received from generator. This MultiPath object contains generated paths for the both asset and volatility (multiPath[0] = asset path, multiPath[1] = volatility path). So, for any further purposes (say, to use simulated asset path for something), one can just use generated asset path and ignore volatility path. However, volatility path is available, if there will be some specific purpose for obtaining the information on how volatility has been evolving over time.

# hard-coded generator for Heston process
def HestonPathGenerator(dates, dayCounter, process, nPaths):
    t = np.array([dayCounter.yearFraction(dates[0], d) for d in dates])
    nGridSteps = (t.shape[0] - 1) * 2
    sequenceGenerator = ql.UniformRandomSequenceGenerator(nGridSteps, ql.UniformRandomGenerator())
    gaussianSequenceGenerator = ql.GaussianRandomSequenceGenerator(sequenceGenerator)
    pathGenerator = ql.GaussianMultiPathGenerator(process, t, gaussianSequenceGenerator, False)
    paths = np.zeros(shape = (nPaths, t.shape[0]))
    
    for i in range(nPaths):
        multiPath = pathGenerator.next().value()
        paths[i,:] = np.array(list(multiPath[0]))
        
    # return array dimensions: [number of paths, number of items in t array]
    return paths

Heston Model Calibration


Below is a simple (hard-coded) method for calibrating Heston model into a given volatility surface. Inside this method, process, model and engine are being created. After this, calibration helpers for Heston model are being created by using given volatility surface data. Finally, calibrated model and process are being returned for any further use. The actual optimization workhorse will be given outside of this method. For this specific example program, SciPy's Differential Evolution solver is being used, in order to guarantee global minimization result.

# hard-coded calibrator for Heston model
def HestonModelCalibrator(valuationDate, calendar, spot, curveHandle, dividendHandle, 
    v0, kappa, theta, sigma, rho, expiration_dates, strikes, data, optimizer, bounds):
    
    # container for heston calibration helpers
    helpers = []
    
    # create Heston process, model and pricing engine
    # use given initial parameters for model
    process = ql.HestonProcess(curveHandle, dividendHandle, 
        ql.QuoteHandle(ql.SimpleQuote(spot)), v0, kappa, theta, sigma, rho)
    model = ql.HestonModel(process)
    engine = ql.AnalyticHestonEngine(model)
    
    # nested cost function for model optimization
    def CostFunction(x):
        parameters = ql.Array(list(x))        
        model.setParams(parameters)
        error = [helper.calibrationError() for helper in helpers]
        return np.sqrt(np.sum(np.abs(error)))

    # create Heston calibration helpers, set pricing engines
    for i in range(len(expiration_dates)):
        for j in range(len(strikes)):
            expiration = expiration_dates[i]
            days = expiration - valuationDate
            period = ql.Period(days, ql.Days)
            vol = data[i][j]
            strike = strikes[j]
            helper = ql.HestonModelHelper(period, calendar, spot, strike,
                ql.QuoteHandle(ql.SimpleQuote(vol)), curveHandle, dividendHandle)
            helper.setPricingEngine(engine)
            helpers.append(helper)
    
    # run optimization, return calibrated model and process
    optimizer(CostFunction, bounds)
    return process, model

Monte Carlo Valuation


The actual Autocallable valuation algorithm has been implemented in this part of the code. For the sake of being able to value this product also after its inception, valuation method takes past fixings as Python dictionary (key: date, value: fixing). Now, if one changes valuation date to any possible date after inception and before transaction maturity, the product will also 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. Algorithm implementation is a bit long and might look scary, but it is actually really straightforward if one is familiar enough with this specific product.

def AutoCallableNote(valuationDate, couponDates, strike, pastFixings, 
    autoCallBarrier, couponBarrier, protectionBarrier, hasMemory, finalRedemptionFormula, 
    coupon, notional, dayCounter, process, generator, nPaths, curve):    
    
    # immediate exit trigger for matured transaction
    if(valuationDate >= couponDates[-1]): return 0.0
    
    # immediate exit trigger for any past autocall event
    if(valuationDate >= couponDates[0]):
        if(max(pastFixings.values()) >= (autoCallBarrier * strike)): return 0.0

    # create date array for path generator
    # combine valuation date and all the 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 = generator(dates, dayCounter, process, nPaths)[:,1:]
    
    # identify the past coupon dates
    pastDates = couponDates[couponDates <= valuationDate]

    # conditionally, merge given past fixings from a given dictionary and generated paths
    if(pastDates.shape[0] > 0):
        pastFixingsArray = np.array([pastFixings[pastDate] for pastDate in pastDates])        
        pastFixingsArray = np.tile(pastFixingsArray, (paths.shape[0], 1))
        paths = np.hstack((pastFixingsArray, paths))
    
    # result accumulator
    global_pv = []
    expirationDate = couponDates[-1]
    hasMemory = int(hasMemory)
    
    # loop through all simulated paths
    for path in paths:
        payoffPV = 0.0
        unpaidCoupons = 0
        hasAutoCalled = False
        
        # loop through set of coupon dates and index ratios
        for date, index in zip(couponDates, (path / strike)):
            # if autocall event has been triggered, immediate exit from this path
            if(hasAutoCalled): break
            payoff = 0.0
                
            # payoff calculation at expiration
            if(date == expirationDate):
                # index is greater or equal to coupon barrier
                # pay 100% redemption, plus coupon, plus conditionally all unpaid coupons
                if(index >= couponBarrier):
                    payoff = notional * (1 + (coupon * (1 + unpaidCoupons * hasMemory)))
                # index is greater or equal to protection barrier and less than coupon barrier
                # pay 100% redemption, no coupon
                if((index >= protectionBarrier) & (index < couponBarrier)):
                    payoff = notional
                # index is less than protection barrier
                # pay redemption according to formula, no coupon
                if(index < protectionBarrier):
                    # note: calculate index value from index ratio
                    index = index * strike
                    payoff = notional * finalRedemptionFormula(index)
                
            # payoff calculation before expiration
            else:
                # index is greater or equal to autocall barrier
                # autocall will happen before expiration
                # pay 100% redemption, plus coupon, plus conditionally all unpaid coupons
                if(index >= autoCallBarrier):
                    payoff = notional * (1 + (coupon * (1 + unpaidCoupons * hasMemory)))
                    hasAutoCalled = True
                # index is greater or equal to coupon barrier and less than autocall barrier
                # autocall will not happen
                # pay coupon, plus conditionally all unpaid coupons
                if((index >= couponBarrier) & (index < autoCallBarrier)):
                    payoff = notional * (coupon * (1 + unpaidCoupons * hasMemory))
                    unpaidCoupons = 0
                # index is less than coupon barrier
                # autocall will not happen
                # no coupon payment, only accumulate unpaid coupons
                if(index < couponBarrier):
                    payoff = 0.0
                    unpaidCoupons += 1                    

            # conditionally, calculate PV for period payoff, add PV to local accumulator
            if(date > valuationDate):
                df = curveHandle.discount(date)
                payoffPV += payoff * df
            
        # add path PV to global accumulator
        global_pv.append(payoffPV)
        
    # return PV
    return np.mean(np.array(global_pv))

Main Program


Finally, in this part of the program we will actually use the stuff presented above. First, the usual QuantLib-related parameters are being created, as well as parameters for Autocallable product. Note, that since in this example we are valuing this product at inception, there is no need to provide any past fixings for valuation method (however, it is not forbidden either). After this, interest rate curve and dividend curve will be created, as well as volatility surface data for calibration purposes. Next, initial guesses for Heston parameters will be set and the model will then be calibrated by using dedicated method. Finally, calibrated process will be given to the actual valuation method and Monte Carlo valuation will be processed.

# general QuantLib-related parameters
valuationDate = ql.Date(20,11,2019)
ql.Settings.instance().evaluationDate = valuationDate
convention = ql.ModifiedFollowing
dayCounter = ql.Actual360()
calendar = ql.TARGET()

# Autocallable Memory Coupon Note
notional = 1000000.0
spot = 3550.0
strike = 3550.0
autoCallBarrier = 1.0
couponBarrier = 0.8
protectionBarrier = 0.6
finalRedemptionFormula = lambda indexAtMaturity: min(1.0, indexAtMaturity / strike)
coupon = 0.05
hasMemory = True

# coupon schedule for note
startDate = ql.Date(20,11,2019)
firstCouponDate = calendar.advance(startDate, ql.Period(1, ql.Years))
lastCouponDate = calendar.advance(startDate, ql.Period(7, ql.Years))
couponDates = np.array(list(ql.Schedule(firstCouponDate, lastCouponDate, ql.Period(ql.Annual), 
    calendar, ql.ModifiedFollowing, ql.ModifiedFollowing, ql.DateGeneration.Forward, False)))

# create past fixings into dictionary
pastFixings = {}
#pastFixings = { ql.Date(20,11,2020): 99.0, ql.Date(22,11,2021): 99.0 }

# create discounting curve and dividend curve, required for Heston model
curveHandle = ql.YieldTermStructureHandle(ql.FlatForward(valuationDate, 0.01, dayCounter))
dividendHandle = ql.YieldTermStructureHandle(ql.FlatForward(valuationDate, 0.0, dayCounter))

# Eurostoxx 50 volatility surface data
expiration_dates = [ql.Date(19,6,2020), ql.Date(18,12,2020), 
    ql.Date(18,6,2021), ql.Date(17,12,2021), ql.Date(17,6,2022),
    ql.Date(16,12,2022), ql.Date(15,12,2023), ql.Date(20,12,2024), 
    ql.Date(19,12,2025), ql.Date(18,12,2026)]

strikes = [3075, 3200, 3350, 3550, 3775, 3950, 4050]

data = [[0.1753, 0.1631, 0.1493, 0.132 , 0.116 , 0.108 , 0.1052],
       [0.1683, 0.1583, 0.147 , 0.1334, 0.1212, 0.1145, 0.1117],
       [0.1673, 0.1597, 0.1517, 0.1428, 0.1346, 0.129 , 0.1262],
       [0.1659, 0.1601, 0.1541, 0.1474, 0.1417, 0.1381, 0.1363],
       [0.1678, 0.1634, 0.1588, 0.1537, 0.1493, 0.1467, 0.1455],
       [0.1678, 0.1644, 0.1609, 0.1572, 0.1541, 0.1522, 0.1513],
       [0.1694, 0.1666, 0.1638, 0.1608, 0.1584, 0.1569, 0.1562],
       [0.1701, 0.168 , 0.166 , 0.164 , 0.1623, 0.1614, 0.161 ],
       [0.1715, 0.1698, 0.1682, 0.1667, 0.1654, 0.1648, 0.1645],
       [0.1724, 0.171 , 0.1697, 0.1684, 0.1675, 0.1671, 0.1669]]

# initial parameters for Heston model
theta = 0.01
kappa = 0.01
sigma = 0.01
rho = 0.01
v0 = 0.01

# bounds for model parameters (1=theta, 2=kappa, 3=sigma, 4=rho, 5=v0)
bounds = [(0.01, 1.0), (0.01, 10.0), (0.01, 1.0), (-1.0, 1.0), (0.01, 1.0)]

# calibrate Heston model, print calibrated parameters
calibrationResult = HestonModelCalibrator(valuationDate, calendar, spot, curveHandle, dividendHandle, 
        v0, kappa, theta, sigma, rho, expiration_dates, strikes, data, opt.differential_evolution, bounds)
print('calibrated Heston parameters', calibrationResult[1].params())

# monte carlo parameters
nPaths = 10000

# request and print PV
PV = AutoCallableNote(valuationDate, couponDates, strike, pastFixings, 
    autoCallBarrier, couponBarrier, protectionBarrier, hasMemory, finalRedemptionFormula, 
    coupon, notional, dayCounter, calibrationResult[0], HestonPathGenerator, nPaths, curveHandle)

print(PV)

Finally, thanks for reading this blog and have a pleasant wait for the coming Christmas.
-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

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