today I want to continue my last post and show you today how to calculate the NPV of a bermudan swaption through Monte-Carlo simulation. The techniques I will show you in this post can be easily extended to simulate exposure paths of a multi callable swap. This exposure simulations can be used for a xVA calculation like the Credit Value Adjustment (CVA) or Potential Future Exposure (PFE).
A physical settled Bermudan swaption is a path dependent derivate. The option has multiple exercise dates and the swaption holder has the right to enter the underlying swap at any of this exercise dates.
On each of the exercise dates the holder have to decide whether it is optimal to exercise the option now or continue to hold the option and may exercise it on a later date.
Lets consider a bermudan swaption with two exercise dates.
At the latest exercise date T the payoff is the well known european swaption payoff
where is the npv of the underlying swap at time t.
At the first exercise date the npv of the swaption given by
and so to the NPV at time 0
One way to solve problem is performing a Monte-Carlo-Simulation. But a naive Monte Carlo approach would require a nested Monte-Carlo Simulation on each path to calculate the continuation value
at time .
Lets say we use 100.000 samples in our simulation, so a bermudan swaption with two exercise dates would require 100.000 x 100.000 samples. Which is very time consuming and grows exponential with the number of exercise dates.
Instead of calculate the continuation value also with a Monte-Carlo simulation we will use a approximation. We use the approximation and algorithm developed by Longstaff and Schwarz.
We approximate the continuation value through some function of some state variable (e.g swap rate, short rate, etc…) :
A common choice for the function is of the form:
with a polynom of the degree .
Lets choice and for our example.
The coefficients of this function f are estimated by the ordinary least square error method. Thats why some people call this method also the OLS Monte-Carlo Method.
So how does the the algorithm look like?
We would simulate n paths of our stochastic process (in our case the short rate process) and go backward through time (backward induction).
We start at the last exercise date and calculate for each path the deflated terminal value of the swaption (exactly like we did in the european case).
Then we go back into time to the next exercise date and approximate the needed continuation value by our ordinary least square estimate.
Our choice for the state variable is the state variable of the short rate process itself.
We choice our coefficient so that the square error of is minimized, where is a vector of all simulated states over all paths and the corresponding vector of the calculated npvs at time T from the previous step.
Now we use this coefficients to calculate the continuation value on each path by inserting the state of the current path into the formula.
On path j the deflated value of the swaption will be
where is the the state of the process at time i on path j.
The npv at time 0 is then
- One could also perform a 2nd MC Simulation (forward in time). First estimate the needed coefficients with a smaller numbers of paths. After train the model with this small set generate a larger sets of paths and go straight forward in time and use the approximation of the continuation value of the first run. On each path you only need to go forward until the holder will exercise the option.
- The exercise time by this approach is only approximatively optimal, since its uses a approximation of the continuation value. Therefore our price will be an (asymptotic) lower bound of the real price.
- The choice of appropriate functions g_i and the degree n is not obvious and can be tricky (see below).
Now lets have a look how this algorithm could be implemented in Python and Quantlib.
We use the notebook from my previous post as our starting point. We use the same yield curves, model (Gaussian short rate model) and the same underlying swap. The underlying 5y swap starts in 1Y.
We setup a bermudan swaption with 2 exercise dates, the first exercise date is on the start date of the swap and the 2nd date on the start date of the 2nd fixed leg accrual period.
Lets add both dates in our list of exercise dates:
calldates = [ settlementDate,
We need to evaluate the value of the underlying swap at both exercise dates on each simulated path. To do that we introduce two new auxiliary functions. The first gives us all future (relative to an arbitrary evaluation time t) payment times and amounts from the fixed rate leg of the swap:
def getFixedLeg(swap, t):
returns all future payment times and amount of the fixed leg of the underlying swap
(np.array, np.array) (times, amounts)
fixed_leg = swap.leg(0)
n = len(fixed_leg)
npv = 0
for i in range(n):
cf = fixed_leg[i]
t_i = timeFromReference(cf.date())
if t_i > t:
return np.array(fixed_times), np.array(fixed_amounts)
The 2nd will give us all future payment times, accrual start and end times, notionals, gearings and day count fractions of the floating leg. We need all this information to estimate the fixing of the float leg.
def getFloatingLeg(swap, t):
float_leg = swap.leg(1)
n = len(float_leg)
float_times = 
float_dcf = 
accrual_start_time = 
accrual_end_time = 
nominals = 
for i in range(n):
# convert base classiborstart_idx Cashflow to
cf = ql.as_floating_rate_coupon(float_leg[i])
value_date = cf.referencePeriodStart()
t_fix_i = timeFromReference(value_date)
t_i = timeFromReference(cf.date())
if t_fix_i >= t:
iborIndex = cf.index()
index_mat = cf.referencePeriodEnd()
# year fraction
# calculate the start and end time
# payment time
return np.array(float_times), np.array(float_dcf), np.array(accrual_start_time), np.array(accrual_end_time), np.array(nominals)
With these two function we can evaluate the the underlying swap given the time and state of the process:
def swapPathNPV(swap, t):
fixed_times, fixed_amounts = getFixedLeg(swap, t)
float_times, float_dcf, accrual_start_time, accrual_end_time, nominals = getFloatingLeg(swap, t)
df_times = np.concatenate([fixed_times,
df_times = np.unique(df_times)
# Store indices of fix leg payment times in
# the df_times array
fix_idx = np.in1d(df_times, fixed_times, True)
fix_idx = fix_idx.nonzero()
# Indices of the floating leg payment times
# in the df_times array
float_idx = np.in1d(df_times, float_times, True)
float_idx = float_idx.nonzero()
# Indices of the accrual start and end time
# in the df_times array
accrual_start_idx = np.in1d(df_times, accrual_start_time, True)
accrual_start_idx = accrual_start_idx.nonzero()
accrual_end_idx = np.in1d(df_times, accrual_end_time, True)
accrual_end_idx = accrual_end_idx.nonzero()
# Calculate NPV
discount = np.vectorize(lambda T: model.zerobond(T, t, x_t))
dfs = discount(df_times)
# Calculate fixed leg npv
fix_leg_npv = np.sum(fixed_amounts * dfs[fix_idx])
# Estimate the index fixings
index_fixings = (dfs[accrual_start_idx] / dfs[accrual_end_idx] - 1)
index_fixings /= float_dcf
# Calculate the floating leg npv
float_leg_npv = np.sum(nominals * index_fixings * float_dcf * dfs[float_idx])
npv = float_leg_npv - fix_leg_npv
This functions returns us a pricing function, which takes the current state of the process to price calculate the NPV of the swap at time t. The technique we use here is called closure function. The inner function is aware of the underlying swap and the ‘fixed’ time t and can access all variables defined in the outer function.
Remark on the pricing function
This is a very simple function. Its not possible to calculate the correct NPV of the swap for a time t between to fixing times of the floating leg. The current floating period will be ignored. So use that function only to evaluate the swap NPV only on fixing dates. We will extend this function to be capable to evaluate the NPV on any time t in the next post.
Use of the function at time t=0
The generation of the time grid hasn’t changed from the last time. It just consists of three points.
Also the generation of our sample path is the same as last time. After we generate our path we calculate the deflated payoffs of our swap at time T:
pricer = np.vectorize(swapPathNPV(swap, time_grid[-1]))
cont_value = pricer(y[:,-1]) / numeraires[:,-1]
cont_value[cont_value < 0] = 0
First we generate a vectorised function of the pricing function and use it on the array of our sample paths y and then apply the maximum function on the result.
In the next step we go one step back in time and calculate the deflated exercise value of the swaption at that time:
pricer = np.vectorize(swapPathNPV(swap, time_grid[-2]))
exercise_values = pricer(y[:,-2]) / numeraires[:,-2]
exercise_values[exercise_values < 0] = 0
Now we estimate the coefficients of continuation value function. We use the library statsmodels and fit an OLS model to the data.
states = y[:, -2]
Y = np.column_stack((states, states**2, states**3, states**4))
Y = sm.add_constant(Y)
ols = sm.OLS(cont_value, Y)
ols_result = ols.fit()
With this coefficients we can now calculate the continuation value on each path, given the state:
cont_value_hat = np.sum(ols_result.params * Y, axis=1)
The deflated value of the swaption at the first exercise is the maximum out of exercise value and continuation value:
npv_amc = np.maximum(cont_value_hat, exercise_values)
The npv at time 0 is the mean of the simulated deflated npvs at the first exercise date times the value of the numeraire at time 0:
npv_amc = np.mean(npv_amc) * numeraires[0,0]
To check the quality of our regression function we can have a look on a scatter plot:
As we can see the regression function doesn’t seems to fit that good to the left tail. So we could either increase the degree of our function, try other polynomial function, change to another state variable or try piecewise regression functions.
As usual you can download the source code from my github account or find it on nbViewer.
In the next post we are going to use this regression based approach to generate exposure paths for a multi callable swap.
I hope you enjoy the post. Till next time.