100% found this document useful (1 vote)
142 views

Practical Solution of Partial Differential Equations in Finance PDF

Uploaded by

yuzukieba
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
100% found this document useful (1 vote)
142 views

Practical Solution of Partial Differential Equations in Finance PDF

Uploaded by

yuzukieba
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 27

Practical Solution of Partial Dierential Equations in Finance

With Numerical Examples and Python Code

Peter Austing
[email protected]

August, 2022

Abstract

It is now possible to numerically solve partial dierential pricing equations so that vanilla
option values are exactly recovered. The nite dierence scheme is not hard to implement,
but it must be done exactly correctly to achieve results to machine precision. We provide a
step by step guide to implementation with numerical examples and python code. We also
correct some formulas in the original proof of Austing (2020), and analyse the impact of
implicit versus semi-implicit stepping.

1 Introduction

In Austing (2020), we demonstrated a nite dierence scheme for solving partial dierential
pricing equations with the property that any vanilla option having its strike and expiry on
the nite dierence grid is exactly recovered. In the intervening period, it has been put to
use at a number of funds and investment banks in asset classes as diverse as foreign exchange,
commodities, equities and interest rate hybrids.

It is important but quite ddly to set up the matrix operators correctly. A number of
readers have highlighted that the structure of the original paper does not ease this process.
In this article we run through a numerical example step by step, and provide python code, to
make it easier to implement the scheme. Our main numerical example covers the particular
case of local volatility with fully implicit stepping. We then present the formulas for the
semi-implicit scheme and investigate the rate of convergence for a barrier option.

Once one has the local volatility case working, it can be extended to other models such
as local-stochastic volatility and three factor interest rate hybrid models as outlined in the
original article.

As with the original paper, our results are set in the context of a number of earlier works.
Two key are Andreasen and Huge (2011a,b). A core principle of the former is that the adjoint

Electronic copy available at: https://ssrn.com/abstract=4194884


1
of a discretized backwards pricing equation is a discretization of the forwards equation. The
authors use this to set up a Monte Carlo simulation with discretization exactly matching
an original nite dierence scheme. We depend on the principle of adjointness throughout.
In the latter work, the authors use a large stride implicit nite dierence scheme with a
numerical solve to interpolate missing implied volatilities on a sparse grid. In our case, the
grid is not sparse, meaning our scheme cannot be used for interpolation. However, that allows
us to solve the system analytically. Meanwhile, Wyns and in't Hout (2018) use the adjointness
principle to set up a discretized local stochastic volatility model with prices matching a given
local volatility discretization. Our approach is similar, but as we have managed to write
down a discretization of Dupire's formula such that vanilla options are exactly repriced, the
corresponding LSV will also reprice vanillas. Finite dierence calibration of local stochastic
volatility models remains of interest to academics as in Saporito, Yang, and Zubelli (2019)
as well as practitioners as in Bang (2019).

In that they achieve exact calibration, our results for the nite dierence approach have
parallels with the powerful particle methods of Guyon and Henry-Labordère (2012) for Monte
Carlo. More recently, there is continued practitioner interest in specic exact discretization
approaches, as in Bang and Daboussi (2022).

2 Solving a PDE

2.1 The problem we will solve


We are going to solve the standard exotic pricing problem. That is, we are given an implied
2
volatility surface, a dividend (or foreign interest rate) yield curve and a domestic interest
rate curve. We will then assume the process follows the Dupire (1992) SDE, of the form

dS
= µdt + σloc (St , t)dW (1)
S
with corresponding backwards pricing equation

∂V ∂V 1 ∂ 2V 2
+ Sµ + S σloc (St , t)2 − V r2 = 0, (2)
∂t ∂S 2 ∂S 2
and forward probability equation

∂ ∂ 1 ∂2 2
q(S, t) = − [Sµq(S, t)] + [S σloc (S, t)2 q(S, t)]. (3)
∂t ∂S 2 ∂S 2
Dupire (1992) tells us that the local volatility function


C(K, T ) ∂
+ Kµ ∂K C(K, T ) + (r2 − µ)C(K, T )
1
σ (K, T )2
2 loc
= ∂T
2

(4)
K 2 ∂K 2 C(K, T )

1 See also Huge (2018).


2 It is more convenient to write `dividend' yield curve so that readers can easily orient by thinking of the

equity pricing case (but note we do not consider the case of discrete dividends). In the FX case, the dividend

yield is, of course, the foreign interest rate.

Electronic copy available at: https://ssrn.com/abstract=4194884


makes the above pricing equation value vanilla options correctly. Here C(K, T ) are the values
of call (or put) options at strikes K and time to expiry T.
Our task is to discretize the pricing equation 2 on a grid so that we can solve it on a computer.
To do so we need a formula for the discretized version of the local volatility function σloc (St , t)
calculated from the market vanilla option prices, and also formulas for the discretized drift
µ and domestic interest rate r2 .
From now on, we will refer to Austing (2020) as A20 and run through a specic example
step by step. For the purpose of illustration, our example will generate the true market
vanilla prices (the values we need to hit) using the Hagen et al. (2002) SABR formula with
parameters

S= 1, Spot

r1 = 0.05, Continuous compounding dividend yield / foreign interest rate

r2 = 0.03, Continuous compounding domestic interest rate

T = 1, Time to expiry in years

α = 0.1, SABR initial vol

ν = 0.5, SABR vol-of-vol

ρ = −0.5, Spot-vol correlation.

We are going to run through a numerical example, in parallel with explaining the steps. This
will help those who are building their own implementations to check each step is correct. We
will only print numbers to around three signicant gures. This should be enough to check
individual implementations are correct, but readers should not copy truncated intermdiate
numbers directly from this article for numerical experiments.

2.2 Step 1: Spot grid


We work in spot space, but the spot grid can be any convenient set of values. For example,
the grid could be uniformly spaced in log-spot space. It is important that any vanilla style
strikes and any continuous barriers are placed on the grid. Digital style strikes, discrete
barriers, and any other discontinuities in the payout, should be placed mid-way between two
grid points. One way to achieve this is to start with a grid that takes no account of trade
features and then deform it, as in Tavella and Randall (2000).

The spot grid is denoted {x1 < x2 < · · · < xn } in A20, and we insist on x1 = 0 and xn
must be large. In practice, large means that a call option with strike xn can be assumed
to have zero value. For our numerical example, we are going to use the following randomly
generated grid
{0, 0.74, 0.78, 0.9, 1.11, 1.23, 1.26, 1.31, 13.11}. (5)

To generate this grid, we rst chose grid boundaries suitable for the problem and then drew
seven random variables between them. Finally we added 0 at the beginning and appended
ten times the nal point at the end.

Electronic copy available at: https://ssrn.com/abstract=4194884


A production implementation would of course not use random grid points; this is simply to
demonstrate that the geometry of the grid is irrelevant. One would also use more grid points
than nine.

2.3 Step 2: Derivative operators


We dene matrices L, D , U via

Li,i−1 = 1/(xi − xi−1 ) , i = 2, · · · , n − 1 (6)

Li,i = −1/(xi+1 − xi ) − 1/(xi − xi−1 ) , i = 2, · · · , n − 1 (7)

Li,i+1 = 1/(xi+1 − xi ) , i = 2, · · · , n (8)

Li,j = 0 , otherwise (9)

Di,i−1 = −1/(xi+1 − xi−1 ) , i = 2, · · · , n − 1 (10)

Di,i = 0 , i = 2, · · · , n − 1 (11)

Di,i+1 = 1/(xi+1 − xi−1 ) , i = 2, · · · , n − 1 (12)

D1,1 = −1/(x2 − x1 ) (13)

D1,2 = 1/(x2 − x1 ) (14)

Dn,n−1 = −1/(xn − xn−1 ) (15)

Dn,n = 1/(xn − xn−1 ) (16)

Di,j = 0 , otherwise (17)

Ui,i = 2/(xi+1 − xi−1 ) , i = 2, · · · , n − 1 (18)

Ui,j = 0 , otherwise (19)

Then UL is our second derivative operator, while D is our rst derivative operator.

It is important to note two dierences between the D matrix dened here compared to A20,
1. In A20, the rst and last row of D are zeros (corresponding to zero boundary condi-
tions). This is an error in A20. Here we have values in those rows corresponding to
zero gamma boundary conditions.

2. In A20, we don't divide by the step sizes; our D corresponds to U D in A20.


In our numerical example, the matrices are
 
0. 0. 0. 0. 0. 0. 0. 0. 0.
1.351 −26.351 25. 0. 0. 0. 0. 0. 0. 
 
 0. 25. −33.333 8.333 0. 0. 0. 0. 0. 
 
 0. 0. 8.333 −13.095 4.762 0. 0. 0. 0. 
 
L=
 0. 0. 0. 4.762 −13.095 8.333 0. 0. 0. 

 0. 0. 0. 0. 8.333 −41.667 33.333 0. 0. 
 
 0. 0. 0. 0. 0. 33.333 −53.333 20. 0. 
 
 0. 0. 0. 0. 0. 0. 20. −20.085 0.085
0. 0. 0. 0. 0. 0. 0. 0. 0.
(20)

Electronic copy available at: https://ssrn.com/abstract=4194884


 
−1.351 1.351 0. 0. 0. 0. 0. 0. 0.
 0. 
−1.282 0. 1.282 0. 0. 0. 0. 0. 
 −6.25 0. 
 0. 0. 6.25 0. 0. 0. 0. 
 0. 0. −3.03 0. 3.03 0. 0. 0. 0. 
 
 0.
D= 0. 0. −3.03 0. 3.03 0. 0. 0.  (21)

 −6.667 0. 
 0. 0. 0. 0. 0. 6.667 0. 
 0. 0. 0. 0. 0. −12.5 0. 12.5 0. 
 
 0. 0. 0. 0. 0. 0. −0.084 0. 0.084
0. 0. 0. 0. 0. 0. 0. −0.085 0.085
 
0. 0. 0. 0. 0. 0. 0. 0. 0.
0. 2.564 0. 0. 0. 0. 0. 0. 0.
 
0. 0. 12.5 0. 0. 0. 0. 0. 0. 
 
0. 0. 0. 6.061 0. 0. 0. 0. 0.
 
U = 0. 0. 0. 0. 6.061 0. 0. 0. 0. 
 (22)
0. 0. 0. 0. 0. 13.333 0. 0. 0.
 
0. 0. 0. 0. 0. 0. 25. 0. 0. 
 
0. 0. 0. 0. 0. 0. 0. 0.169 0.
0. 0. 0. 0. 0. 0. 0. 0. 0.

2.4 Step 3: compute discretized dividend yield and interest rate


Here we simply quote from A20. The discretized domestic interest rates applying for the
fully implicit case are
(df τ −1,τ )−1 − 1
rτ = , (23)
∆t
and the dividend yields
τ −1,τ −1
τ (dfdiv ) −1
d = , (24)
∆t
where the index τ = 1, · · · , ntime represent the time grid points. The drifts are given by

µτ = r τ − d τ . (25)

In our numerical example, the continuous compounding rates r2 = 0.03 and r1 = 0.05 lead
to discretized interest rate and drift

rτ = 0.030045045 , τ = 1, · · · , 10 (26)

µτ = −0.020080164 , τ = 1, · · · , 10 (27)

2.5 Aside 1: The inverse of L and its relation to probabilities and


vanilla option prices
We dene a matrix L−1 by
(L−1 )i,j = max(xi − xj , 0). (28)

Electronic copy available at: https://ssrn.com/abstract=4194884


It is not a true inverse of L, but satises two relations,

(LL−1 )ij = (diag {0, 1, · · · , 1, 0})ij (29)

3
and
(L−1 L† )ij = (diag {0, 1, · · · , 1, 0})ij . (30)

Let us dene, for a given expiry, {ci } to be market call option prices with strikes on the grid
points xi , and {pi } to be market put option prices with strikes xi . Then

q = Lc ≡ Lp (31)

are discounted risk-neutral probabilities, with the property


n
qi = df (32)
i=1

where df is the discount factor. The relation Lc ≡ Lp is a simple consequence of put-call


parity, since L annihilates the vector x.
Note 1: There is a dierence in notation here versus A20. In A20 p are probabilities, while
here p are put prices and q are probabilities.

If we apply L −1
to the probability vector q, we retrieve put prices
L−1 q = p. (33)

Note 2: in A20 there is confusion between L and L† with the result that this relation
is incorrectly asserted to retrieve call prices (rather than put prices) just before equation
A20.16.
In our numerical example, the inverse L matrix is

 
0. 0. 0. 0. 0. 0. 0. 0. 0.
 0.74 0. 0. 0. 0. 0. 0. 0. 0.
 
 0.78 0.04 0. 0. 0. 0. 0. 0. 0.
 
 0.9 0.16 0.12 0. 0. 0. 0. 0. 0.
 
L−1 
=  1.11 0.37 0.33 0.21 0. 0. 0. 0. 0. (34)

 1.23 0.49 0.45 0.33 0.12 0. 0. 0. 0.
 
 1.26 0.52 0.48 0.36 0.15 0.03 0. 0. 0.
 
 1.31 0.57 0.53 0.41 0.2 0.08 0.05 0. 0.
13.11 12.37 12.33 12.21 12. 11.88 11.85 11.8 0.

2.6 Aside 2: Deriving the local volatility formula


The discretized local volatility formula arrived at in A20 is correct, but the proof has an
error. For good order, we correct that here.

3 We denote matrix transpose by † instead of T to avoid confusion with expiry time.

Electronic copy available at: https://ssrn.com/abstract=4194884


We rst discretize the forward probability equation via

(q τ +1 − q τ ) 1
= D† Xµτ +1 q τ +1 + L† X 2 V τ +1 U q τ +1 − rτ +1 q τ +1 , τ = 0, · · · , ntime − 1. (35)
∆t 2
This is a fully implicit scheme because we use time indices τ + 1 (rather than τ ) on the right
hand side. We are using notation q for (discounted) probabilities so that we free up p for put
option prices. This leads to an inconsistency of notation versus A20, but will hopefully not
be too confusing. X is the diagonal matrix formed from the spot grid X = diag ({xi }) and
V τ = diag ({(σiτ )2 }) is a diagonal matrix of local volatility squares and is to be determined.
Since q 1 ≡ qn ≡ 0 , we will set V11 = Vnn = 0.
In addition, our formula 35 lacks the matrix U in the rst term on the right compared to
A20.15 because we included U in our denition of D.
As in A20, we apply L to both sides of 35, but this time use the correct relations q = Lp
−1

and p = L−1 q to obtain

(pτ +1 − pτ )i 1
= (L−1 D† XµLpτ +1 + X 2 V U Lpτ +1 − rpτ +1 )i , 2 ≤ i ≤ n − 1. (36)
∆t 2
We do not want to use this formula directly to calculate the local volatilities because L−1 is
triangular requiring O(n ) operations for multiplication.
2

Fortunately, the matrix L−1 D† XL is almost tri-diagonal. The only elements not on the three
major diagonals are in the rst column. But they are irrelevant because p1 = 0 (the value
of a put option with strike zero).

We dene the matrix J by

xi (xi+1 − xi )
Ji,i−1 = − (37)
(xi+1 − xi−1 )(xi − xi−1 )
xi (xi+1 − xi ) − xi+1 (xi − xi−1 )
Ji,i = (38)
(xi+1 − xi )(xi − xi−1 )
xi (xi − xi−1 )
Ji,i+1 = (39)
(xi+1 − xi−1 )(xi+1 − xi )
Jij = 0 , otherwise. (40)

which is equal to the tri-diagonal part of −L−1 D† XL, plus two elements in the bottom right
corner (which are irrelevant because the local variance vector V has Vn = 0). Then, as in
A20, we arrive at the local volatility formula
(pτi +1 − pτi )/∆t + (µJ + r)pτi +1
(σiτ )2 = 1 , 2≤i≤n−1 (41)
2
(xi )2 (U Lpτ +1 )i
(σiτ )2 = 0 , otherwise. (42)

In this formula we use put values p, while A20.23 uses call values. Both versions work
numerically, and one can even switch from using puts to calls half way along the grid. No

Electronic copy available at: https://ssrn.com/abstract=4194884


doubt careful manipulation of the matrices will lead to a proof of this, but we leave it as an
exercise for an interested reader.

To understand the matrix J, we can compare 41 with its continuous counterpart 4. We see
that J is simply a (rather non-intuitive) discretization of S

∂S
− 1.
In our numerical example, we have

L−1 D† XL =
 
0 0 0 0 0 0 0 0 0
−0.9487 18.50 −17.55 0 0 0 0 0 0 
 
 −1 14.62 −12. −1.625 0 0 0 0 0 
 
 −1 0 4.773 −2.214 −1.558 0 0 0 0 
 
 −1 0 0 1.922 4.964 −5.886 0 0 0 
 
 −1 0 0 0 2.050 31.75 −32.80 0 0 
 
 −1 0 0 0 0 26.25 −15.80 −9.450 0 
 
 −1 0 0 0 0 0 26.09 −25.09 −0.0004684
−1 0 0 0 0 0 0 1.111 −0.111
(43)
and

J=
 
0 0 0 0 0 0 0 0 0
−0.05128 −18.50 17.55 0 0 0 0 0 0 
 
 0 −14.62 12.00 1.625 0 0 0 0 0 
 
 0 0 −4.773 2.214 1.558 0 0 0 0 
 
 0 0 0 −1.922 −4.964 5.886 0 0 0 
 
 0 0 0 0 −2.050 −31.75 32.80 0 0 
 
 0 0 0 0 0 −26.25 15.80 9.450 0 
 
 0 0 0 0 0 0 −26.09 25.09 0.0004684
0 0 0 0 0 0 0 0 0
(44)
and nally, their dierence,

 
0 0 0 0 0 0 0 0 0
−1 0 0 0 0 0 0 0 0 
 
−1 0 0 0 0 0 0 0 0 
 
−1 0 0 0 0 0 0 0 0 
 
L D XL − (−J) = 
−1 †
−1 0 0 0 0 0 0 0 0   (45)
−1 0 0 0 0 0 0 0 0 
 
−1 0 0 0 0 0 0 0 0 
 
−1 0 0 0 0 0 0 0 0 
−1 0 0 0 0 0 0 1.111 −0.111

Electronic copy available at: https://ssrn.com/abstract=4194884


2.7 Step 4: Compute the local volatilities
We can now use the formula 41 to compute the array of local volatilities. In our numerical
example, we divide the one year expiry into ten time intervals each having ∆t = 0.1, and
arrive at put option values

 
0. 0. 0. 0. 0.11 0.23 0.26 0.31 12.11
0. 0. 0. 0. 0.112 0.231 0.261 0.311 12.076
 
0. 0. 0. 0. 0.113 0.233 0.262 0.312 12.042
 
0. 0. 0. 0.001 0.115 0.234 0.264 0.313 12.007
 
0. 0. 0. 0.003 0.117 0.235 0.265 0.314 11.973
 

p = 0. 0. 0. 0.004 0.119 0.236 0.266 0.315 11.94 
. (46)
0. 0. 0. 0.006 0.121 0.238 0.267 0.316 11.906
 
0. 0. 0.001 0.008 0.123 0.239 0.268 0.317 11.872
 
0. 0.001 0.001 0.01 0.125 0.24 0.269 0.318 11.838
 
0. 0.001 0.002 0.011 0.127 0.241 0.271 0.319 11.805
0. 0.001 0.002 0.013 0.13 0.243 0.272 0.32 11.771

In this matrix (pτ )i , the rst row are the values of put options at expiry time 0 for the ten
strikes and the nal row are the values at time to expiry 1 year. A time grid with 10 intervals
has 11 grid points, and so the index τ runs from 0 to 10 with 0 corresponding to time zero.
Later, we are going to solve the pricing PDE to recover the value of the put option which
has position p10,5 ≡ 0.13 in this matrix. These numbers have been rounded to three decimal
places, so we point out now that the full precision of this number calculated with the code
listing below on my PC was 0.12956379093157036.
The local volatility formula is (41)

(pτi +1 − pτi )/∆t + (µJ + r)pτi +1


(σiτ )2 = 1 , 2≤i≤n−1 (47)
2
(xi )2 (U Lpτ +1 )i
(σiτ )2 = 0 , otherwise. (48)

and this gives us the local volatility squared matrix

 
0. −0.017 −0.001 −0.003 0.002 0. 0.004 0. 0.
0. 0.023 −0.001 0. 0.002 0. 0.002 0.213 0.
 
0. 0.089 0.001 0.005 0.003 0.001 0.004 0.331 0. 
 
0. 0.147 0.005 0.008 0.004 0.001 0.006 0.512 0. 
 
 0. 0.18 0.009 0.01 0.005 0.001 0.008 0.695 0.
2 τ 
(σ ) i =  . (49)
0. 0.195 0.012 0.011 0.005 0.002 0.01 0.837 0. 
 
0. 0.2 0.015 0.012 0.006 0.003 0.011 0.934 0.
 
0. 0.198 0.017 0.012 0.006 0.003 0.011 0.993 0. 
 
0. 0.194 0.019 0.012 0.006 0.004 0.012 1.026 0.
0. 0.188 0.02 0.013 0.006 0.004 0.013 1.042 0.

This matrix has 10 rows, because local volatilities apply to time intervals, not to time points.

Electronic copy available at: https://ssrn.com/abstract=4194884


Note that some of the local vol-squares are negative. This has happened in the rst two
rows (early times) for strikes with very small probability. In a production implementation,
negative local vol squares should always be oored at zero to avoid potential instability of
the Thomas (1949) algorithm when stepping implicitly. This will have a (tiny) eect on the
re-pricing of vanillas, so we have not oored them for the purpose of this demonstration.

2.8 Step 5: Solve the PDE


We start with solution vector equal to the payo of the option on the spot grid. For our
example, we chose to value a put option with strike 1.11, which of course we chose as one of
the spot grid points). Then the payo vector is

 
1.11
0.37
 
0.33
 
0.21
 
v=
 0 .
 (50)
 0 
 
 0 
 
 0 
0

Our discretization of the pricing PDE 2 is A20 equation A20.32,


v τ −1 − v τ 1
= (µτ XD + V τ X 2 U L − rτ )v τ −1 . (51)
∆t 2
Here we changed notation from q in A20 to v here for the solution vector (since we already
used q for discounted probabilities), and the rst term on the right hand side lacks the matrix
U since we included it in D here.
We dene the matrix Qτ = (µτ XD + 12 V τ X 2 U L − rτ ) so that

v τ = (1 − ∆tQτ )v τ −1 . (52)

τ τ −1
Since we are trying to step backwards from v to v , we need to invert the tridiagonal
matrix A ≡ (1 − ∆tQ ) using a tridiagonal matrix inversion such as the Thomas (1949)
τ τ

algorithm.

In our numerical example, we have 10 time intervals, so we begin with the matrix A10 , which

10

Electronic copy available at: https://ssrn.com/abstract=4194884


has numerical values
 
1.003 0. 0. 0. 0. 0. 0. 0. 0.
−0.02 1.351 −0.329 0. 0. 0. 0. 0. 0. 
 
 0. −0.202 1.26 −0.054 0. 0. 0. 0. 0. 
 
 0. 0. −0.031 1.044 −0.009 0. 0. 0. 0. 
 
A10 =
 0. 0. 0. −0.018 1.034 −0.013 0. 0. 0. .

 0. 0. 0. 0. −0.051 1.177 −0.123 0. 0. 
 
 0. 0. 0. 0. 0. −0.864 2.334 −0.468 0. 
 
 0. 0. 0. 0. 0. 0. −0.302 1.306 −0.001
0. 0. 0. 0. 0. 0. 0. −0.002 1.005
(53)
Stepping back once leads to v 9 = (A10 )−1 v 10 where v 10 is the initial solution v in equation
50. This gives
 
1.107
 0.37 
 
0.331
 
0.211
 
v9 = 
 0.004 .
 (54)
 0. 
 
 0. 
 
 0. 
0.
By the time we step back all the way to time 0, the solution vector is

 
1.077
0.373
 
0.335
 
0.221
 
v0 = 
 0.029 .
 (55)
0.004
 
0.002
 
0.001
0.

2.9 Step 6: Interpolating the solution


0 0 0
To interpolate the solution vector v onto the market spot rate, we dot product v with q .
Here q ≡ Lp is the vector of probabilities derived from the put (or call) option payos with
0 0

11

Electronic copy available at: https://ssrn.com/abstract=4194884


zero time to expiry. In our example, we have

 
0.
 0. 
 
 0. 
 
0.524
 
q0 =  
0.476 (56)
 0. 
 
 0. 
 
 0. 
0.

leading to nal solution


(q 0 )† v 0 = 0.1295637909315702. (57)

On my PC, using the python code listing in section 5, this has error compared to the true
value listed earlier (after equation 46) only in the 16th digit.

3 Extension to semi-implicit scheme

Up to now, we have used a fully implicit scheme. We will now extend to the Crank and
Nicolson (1947) semi-implicit approach by introducing a parameter θ so that the discretized
pricing equation becomes

v τ −1 − v τ 1
= (µτ XD + V τ X 2 U L − rτ )(θv τ −1 + (1 − θ)v τ ). (58)
∆t 2
with θ = 1 being implicit, θ = 0.5 semi-implicit and θ = 0 explicit. Duy (2004) has
pointed out that great care is required when using the CrankNicolson scheme. It can
lead to oscillating errors if used near discontinuities in the payout or boundary conditions.
However, it is popular to do a few smaller fully implicit steps after any discontinuity, and
semi-implicit steps otherwise. This is known as Rannacher (1984) marching. For analysis of
the convergence of standard schemes with this approach, readers are referred to Giles and
Carter (2005).

We rst need to adjust the formulas for the discretized interest rate, dividend yield, and
local volatility. For the interest rate, as explained in section 4 of A20, we consider valuing a
cash payment, whose payout is represented by a vector of all ones, v = (1, · · · , 1)† . As both
D and L annihilate v, the discretized pricing equation 51 becomes

v τ −1 − v τ
= −rτ (θv τ −1 + (1 − θ)v τ ). (59)
∆t
Since v τ −1 is equal to vτ discounted back by discount factor df τ −1,τ , this tells us that

1 − df τ −1,τ
rτ = (60)
∆t(θdf τ −1,τ + 1 − θ)

12

Electronic copy available at: https://ssrn.com/abstract=4194884


If we price instead a simple strikeless forward contract, the solution vector at τ is v =
(x1 , · · · , xn )† . This is annihilated by L, but satises XDv = v . We represent our discretized
τ τ τ
drift µ as the dierence between the dividend rate d and domestic interest rate r . Then
the discretized pricing equation becomes

v τ −1 − v τ
= −dτ (θv τ −1 + (1 − θ)v τ ). (61)
∆t
so that
τ −1,τ
τ 1 − dfdiv
d = τ −1,τ , (62)
∆t(θdfdiv + 1 − θ)
where dfdiv is the dividend yield discount factor between τ −1 and τ.
The discretized local volatility formula is a simple extension of 41,

(pτi +1 − pτi )/∆t + (µJ + r)(θpτ +1 + (1 − θ)pτ )i


(σiτ )2 = , 2≤i≤n−1 (63)
1
2
(xi )2 (U L(θpτ +1 + (1 − θ)pτ )i
(σiτ )2 = 0 , otherwise. (64)

Finally, to step back the PDE, we retain our denition of the matrix Qτ = (µτ XD +
V X 2 U L − rτ ). Then the pricing equation becomes
1 τ
2

v τ −1 − v τ
= Qτ (θv τ −1 + (1 − θ)v τ ), (65)
∆t
and we step backwards using

v τ −1 = (1 − θ∆tQτ )−1 (1 + (1 − θ)∆tQτ )v τ . (66)

You can try this out by adjusting the value of the variable theta in the python code listed
in section 5.

4 Implicit versus semi-implicit stepping

Since vanilla options are exactly re-priced by construction, it is interesting to consider


whether there is any benet in semi-implicit stepping. Is it possible that nailing down vanilla
prices eectively corrects for time-stepping errors, even when the contract is not vanilla?

The purest way to investigate is by valuing a no-touch option. This is a contract that pays
cash as long as a continuous barrier is not breached. We have included this experiment in the
code listing. This time, we consider a case with constant implied volatility of 10% by setting
the SABR vol of vol to zero, which allows us to compare against the analytic BlackScholes
PV available in Haug (1997). We set the barrier to 1.06 so that the PV of the option is
approximately 50% of the notional. We use a spot grid that is uniform in spot space and
goes from minus three standard deviations up to the barrier level, and we of course add
zero and a large value to the grid. When it comes to pricing, we take care to remove the

13

Electronic copy available at: https://ssrn.com/abstract=4194884


large grid point and set the last row of D and L to zero so that the boundary condition is
correct. We used a dense 500 spot step spot grid to make sure that we are studying time
grid convergence alone.

The conclusion is that semi-implicit stepping converges much faster than fully implicit. With
only 20 time steps, the semi-implicit method has an error smaller than the fully implicit
reaches even with 500 time steps. The results are collated in table 1 and plotted in gure 1,
scaled up to a realistic no-touch with $1,000,000 cash payment upon touch, in both dollars
and in basis points.

Error in $ Error in basis points


Time steps Semi-implicit Fully-implicit Semi-implicit Fully-implicit
10 -292.2 3,023 -2.922 30.24
20 -37.4 1,551 -0.374 15.52
50 -7.4 618 -0.074 6.19
100 -3.9 307 -0.039 3.08
200 -3.0 152 -0.030 1.52
500 -2.7 59 -0.027 0.59

Table 1: Pricing error for a $1m no-touch

$error for a $1m no-touch


3,500

3,000

2,500
Semi-implicit
2,000
Fully-implicit
1,500

1,000

500

0
0 50 100 150 200 250 300 350 400 450 500
-500
Number of me steps

Figure 1: $error for a $1m no-touch option

14

Electronic copy available at: https://ssrn.com/abstract=4194884


5 Python code listing

The results in the preceding section were generated with the following code. I will get around
to pushing this code to a public git repository, but in the meantime please email me for a
text le with the code (since copy / paste from this pdf will be very painful).

1 """

2 Demonstration of :

3 Finite Difference Schemes with Exact Recovery of Vanilla Option Prices

4 ( Risk Nov 2020)

5 h t t p s : / / p a p e r s . s s r n . com/ s o l 3 / p a p e r s . cfm ? a b s t r a c t _ i d = 3 5 3 0 5 6 1

6
7 Note : this is not intended to be efficient or production quality .

8 It is just to demonstrate the algorithm so you can implement your

9 own code easily

10
11 Copyright 2022 Peter Austing

12
13 You may use this code freely , but please only distribute it by referencing the

14 article in which it is listed .

15
16 """

17
18 import numpy as np

19 import pandas as pd

20 import scipy . l i n a l g . lapack

21 import math

22 from scipy . stats import norm

23 import time

24 import inspect

25
26 def tridiagonalMatrix (a , b, c) :

27 """ C r e a t e s a tridiagonal matrix , represented as

28
29 | b0 c0 0 0 0 . . . 0 0|

30 | a1 b1 c1 0 0 . . . |

31 | 0 a2 b2 c2 0 . . . |

32 | 0 0 a3 b3 c3 . . . |

33 | . . . |

34 | 0 0 0 0 0 . . . an =1 bn =1|
35
36 :a: lower diagonal

37 :b: main diagonal

38 : c : upper diagonal

39 : returns : tridiagonal matrix

40 """

41 return np . d i a g ( a [ 1 : ] , =1) + np . d i a g ( b , 0) + np . d i a g ( c [ 0 : =1] , 1)

42
43 def diagonalMatrix (d) :

44 """ C r e a t e s a diagonal matrix

45
46 :d: diagonal

47 : returns : diagonal matrix

15

Electronic copy available at: https://ssrn.com/abstract=4194884


48 """

49 return np . d i a g ( d , 0)

50
51
52 def i n v e r t A n d M u l t i p l y (T, x ) :

53 """ M u l t i p l y inverse of tridiagonal matrix T by vector x

54
55 :T: tridiagonal matrix

56 :x: list or vector

57 : returns : inverse of T multiplied by x

58 """

59 dl = np . d i a g o n a l ( T, =1)
60 d = np . d i a g o n a l ( T , 0 )

61 du = np . d i a g o n a l ( T , 1 )

62 # Note : dgtsv may ( I think ) be made more efficient if you allow

63 # to overwrite dl , d, du by supplying optional params

64 res = s c i p y . l i n a l g . lapack . dgtsv ( dl , d, du , x)

65 return res [ 3 ]

66
67 def getLOperator ( g r i d ) :

68 " " " Get the second order derivative matrix operator L

69
70 : grid : spot grid

71 : returns : matrix L

72 """

73 n = len ( grid )

74 a , b, c = [0] * n, [0] * n, [0] * n

75 for r in range (1 , n = 1) :
76 dxup = grid [ r + 1] = grid [ r ] ;

77 dxdw = grid [ r ] = grid [ r = 1];

78 x = 1. / dxup ;

79 y = = 1. / dxup = 1. / dxdw ;

80 z = 1. / dxdw ;

81 b[ r ] = y;

82 a[ r ] = z ;

83 c [ r ] = x;

84 L = tridiagonalMatrix (a , b, c) ;

85 return L

86
87 def getLInverseOperator ( grid ) :

88 " " " Get the ' inverse ' of the second order derivative matrix L

89 It is not a true matrix inverse , but is the inverse on the space we care

about

90
91 : grid : spot grid

92 : returns : inverse of L

93 """

94 n = len ( grid )

95 M = np . z e r o s ( ( n , n ) )

96 for i in range (0 , n) :

97 for j in range (0 , n) :

98 M[ i ] [ j ] = max ( g r i d [ i ] =grid [ j ] ,0)

99 return M

100

16

Electronic copy available at: https://ssrn.com/abstract=4194884


101 def getJOperator ( grid ) :

102 " " " Get the tridiagonal matrix J

103
104 : grid : spot grid

105 : returns : J matrix

106 """

107 n = len ( grid )

108 a ,b, c = [0] *


* n, [0] * n
n, [0]

109 for i in = 1) :
range (1 , n

110 a [ i ] = =g r i d [ i ] * ( g r i d [ i + 1 ] = g r i d [ i ] ) / ( ( g r i d [ i + 1 ] = g r i d [ i =

1]) * ( grid [ i ] = grid [ i = 1]) )

111 b [ i ] = ( grid [ i ] * ( grid [ i + 1] = grid [ i ] ) = grid [ i + 1] * ( grid [ i ] =

grid [ i = 1]) ) / (( grid [ i + 1] = grid [ i ] ) * ( grid [ i ] = grid [ i = 1]) )

112 c [ i ] = grid [ i ] * ( grid [ i ] = grid [ i = 1]) / (( grid [ i + 1] = grid [ i =

1]) * ( grid [ i + 1] = grid [ i ] ) )

113 t = tridiagonalMatrix (a , b, c)

114 return t

115
116 def getUOperator ( g r i d ) :

117 " " " Get the diagonal matrix U

118
119 : grid : spot grid

120 : returns : U matrix

121 """

122
123 n = len ( grid )

124 d = [0] * n

125 for r in range (1 , n = 1) :


126 dx = 0.5 * ( grid [ r + 1] = grid [ r = 1])

127 d[ r ] = 1. / dx

128 t = diagonalMatrix (d) ;

129 return t

130
131 def getDOperator ( g r i d ) :

132 " " " Get the first order derivative operator matrix D

133
134 : grid : spot grid

135 : returns : D matrix

136 """

137 n = len ( grid )

138 a , b, c = [0] * n, [0] * n, [0] * n

139 for r in range (1 , n= 1) :


140 dx = grid [ r + 1] = grid [ r = 1]

141 x = 1. / dx

142 a[ r ] = =x
143 b[ r ] = 0.

144 c [ r ] = x

145
146 dx = grid [ 1 ] = grid [ 0 ]

147 b[0] = = 1. / dx

148 c [0] = 1. / dx

149 dx = grid [ n =1] = grid [ n =2]


150 a[n = 1] = = 1. / dx

151 b[n = 1] = 1. / dx

17

Electronic copy available at: https://ssrn.com/abstract=4194884


152 t = tridiagonalMatrix (a , b, c)

153 return t

154
155
156
157 def s a b r V o l s ( alpha , nu , rho , T, fwd , strike_grid ) :

158 " " "SABR formula to calculate volatilities on a strike grid

159 Assumes beta = 1 ( log =n o r m a l )


160
161 : alpha : SABR initial vol

162 : nu : SABR vol =o f = v o l


163 : rho : SABR s p o t =v o l correlation

164 :T: time to expiry in years

165 : fwd : forward rate

166 : strike_grid : numpy array of strikes

167 : returns : numpy array of volatilities

168 """

169 n = len ( strike_grid )

170 res = np . empty ( n )

171 if ( nu == 0.) :

172 r e s . f i l l ( alpha )

173 return res

174 log_moneyness = np . l o g ( s t r i k e _ g r i d / fwd )

175 z = ( = nu / alpha ) * log_moneyness ;

176 chi = np . l o g ( ( np . s q r t ( 1 . = 2 . * rho * z + z * z ) + z = rho ) / ( 1 . = rho ) )


177 sigma = alpha * z / chi * ( 1 + ( 0 . 2 5 * r h o * nu * a l p h a + ( 2 . = 3 . * r h o *
rho ) / 24. * nu * nu ) * T)

178 return sigma

179
180 def b l a c k S c h o l e s P V ( fwd , t , strike_grid , vol , is_call ) :

181 """ Black Scholes formula

182
183 : fwd : float forward rate

184 : t : float time to expiry

185 : strike_grid : numpy array of float strikes

186 : vol : numpy array offloat strikes

187 : is_call : boolean true for call , false for put

188 : returns : numpy array of forward values

189 """

190 if t == 0.0:

191 calls = np . w h e r e ( fwd =s t r i k e _ g r i d >0 , fwd =s t r i k e _ g r i d ,0)

192 else :

193 vrt = vol * math . s q r t ( t )

194 log_moneyness = np . l o g ( s t r i k e _ g r i d / fwd )

195 d1 = (0.5 * vol * vol * t = log_moneyness ) / vrt

196 d2 = d1 = vrt

197 fnd1 = fwd * norm . c d f ( d1 )

198 calls = fnd1 = strike_grid * norm . c d f ( d2 )

199
200 if is_call :

201 return calls

202
203 puts = calls + strike_grid = fwd

204 return puts

18

Electronic copy available at: https://ssrn.com/abstract=4194884


205
206 def g e t _ g r i d ( method , lower_bndy , upper_bndy , n_points , decimal_places = None ) :

207 """ g e n e r a t e a spot grid

208
209 : method : string ' linear ' , ' random '

210 : lower_bndy : float lower boundary

211 : upper_bndy : float upper boundary

212 : n_points : number of points

213 : decimal_places : None , or round to this number of decimals

214 : returns : numpy array of grid points

215 """

216 grid = None

217 if method == ' linear ' :

218 grid = np . l i n s p a c e ( l o w e r _ b n d y , upper_bndy , n_points = 2)

219 elif method == ' random ' :

220 grid = np . random . d e f a u l t _ r n g ( s e e d =13) . u n i f o r m ( l o w e r _ b n d y , upper_bndy ,

n_points = 2)

221 grid = np . s o r t ( grid )

222
223 # The grid must contain zero , and a large number

224 grid = np . i n s e r t ( g r i d , 0, 0)

225 # We use 10 times upper_bndy for the ' large ' value .

226 # All that matters is the PV of a call must be very close to zero at this

strike

227 grid = np . a p p e n d ( g r i d , 10. * grid [ =1])


228
229 if not decimal_places is None :

230 # Optionally , round strikes to decimal_places

231 # just to make the accompanying article easier to read

232 grid = [ round ( x , decimal_places ) for x in grid ]

233 grid = l i s t ( set ( grid ) ) # remove duplicates

234 grid = np . s o r t ( g r i d )

235
236 return grid

237
238 def retrieve_name ( var ) :

239 " " "A function to look in the call stack and get the name of a variable 2

frames

240 back . This allows us to print the name of an array when we print its

values

241
242 : var : the variable

243 : returns : string name of variable

244 """

245 callers_local_vars = i n s p e c t . c u r r e n t f r a m e ( ) . f_back . f_back . f _ l o c a l s . i t e m s ( )

246 name = [ var_name for var_name , var_val in callers_local_vars if var_val is

var ]

247 res = name [ 0 ] if l e n ( name ) > 0 else ' Unknown '

248 return res

249
250 def aprint ( array ) :

251 """ P r i n t an array in nice format ( by converting to a DataFrame ) .

252 Tiny numbers are set to exactly zero

253

19

Electronic copy available at: https://ssrn.com/abstract=4194884


254 : array : the array

255 """

256 M = a r r a y . copy ( )

257 M[ a b s (M) < 1. e =11] = 0

258 df = pd . DataFrame (M)

259 name = retrieve_name ( array )

260 p r i n t ( name + ' : ')

261 print ( df )

262
263 def l p r i n t ( array , precision = None ) :

264 """ P r i n t s array in LaTeX format

265
266 : array : numpy array

267 : returns : LaTeX bmatrix as a string

268 """

269 if l e n ( array . shape ) > 2:

270 raise ValueError ( ' bmatrix can at most display two dimensions ' )

271 np . s e t _ p r i n t o p t i o n s ( p r e c i s i o n =p r e c i s i o n , s u p p r e s s=T r u e ) # Set formatting

272 lines = s t r ( a r r a y ) . r e p l a c e ( ' \n ' , ' ' ) . r e p l a c e ( ' [ ' , ' ' ) . replace ( ' ] ' , ' \n ' ) .

s p l i t l i n e s ()

273 rv = [ r ' \ begin { bmatrix } ' ]

274 rv += [ ' ' + ' & ' . join ( l . s p l i t () ) + r ' \\ ' for l in lines ]

275 rv += [ r ' \ end { b m a t r i x } ' ]

276 np . s e t _ p r i n t o p t i o n s ( )

277 name = retrieve_name ( array )

278 res = ' \n ' . j o i n ( r v )

279 p r i n t ( name + ' = ' + res )

280
281 def noTouchBlackPV ( s p o t , r1 , r2 , sigma , T, barrier ) :

282 """

283 Black =S c h o l e s PV of option that pays 1 if upper continuous barrier is not

284 touched . See The Complete Guide to Option Pricing Formulas by Espen

Gaarder

285 Haug 2 nd Ed p177

286
287 : spot : float

288 : r1 : float dividend yield / foreign interest rate

289 : r2 : float domestic interest rate

290 : sigma : float implied volatility

291 :T: float time to expiry in years

292 : barrier : float barrier level

293 : returns : PV

294 """

295 S = spot ; H = barrier ; b = r2 = r1

296 sigmasqr = sigma * sigma ; sigmart = sigma * math . s q r t (T)

297 K = 1 # option pays 1 unit of cash

298
299 if S > H:

300 return 0.0

301
302 eta = = 1; phi = =1 # Case 10 in Haug book

303 mu = (b = 0.5 * sigmasqr ) / sigmasqr

304 x2 = math . l o g ( S / H) / s i g m a r t + (mu + 1) * sigmart

305 y2 = math . l o g (H / S) / sigmart + (mu + 1) * sigmart

20

Electronic copy available at: https://ssrn.com/abstract=4194884


306
307 B2 = K * math . e x p ( =r2 * T) * norm . c d f ( p h i * x2 = phi * sigmart )
308 B4 = K * math . e x p ( =r2 * T) * math . pow (H / S, 2. * mu) * norm . c d f ( e t a * y2

= eta * sigmart )

309
310 return B2 = B4

311
312 def getDiscretizedParams ( strike_grid , time_grid , local_vars , drifts , d o m _r a t e s

, p0 , remove_last_point = False , floor_at_zero = False ) :

313 # L is the second order differential operator

314 L = getLOperator ( s t r i k e _ g r i d )

315
316 # J is the tridiagonal matrix that appears in the numerator of the special

317 # discretisation of Dupire ' s formula

318 J = getJOperator ( s tri ke_ gri d )

319
320 U = getUOperator ( s t r i k e _ g r i d )

321
322 D = getDOperator ( s t r i k e _ g r i d )

323
324 # S is a diagonal matrix representing ' spot ' in differential equations

325 S = diagonalMatrix ( strike_grid )

326
327 Ssqr = np . matmul ( S , S)

328
329 n = len ( strike_grid )

330 if remove_last_point :

331 n == 1

332
333 # Set up the array of true market vanilla prices

334 # We generate them from a SABR model for demonstration purposes

335 use_calls = False

336 call_prices = []

337 for j in range (0 , ntime + 1) :

338 t = time_grid [ j ]

339 fwd = spot * math . e x p ( ( r 2 = r1 ) * t) # forward

340 df = math . e x p ( =r2 * t) # discount factor

341 vols = s a b r V o l s ( alpha , nu , rho , t , fwd , strike_grid [1: =1])


342 calls = df * b l a c k S c h o l e s P V ( fwd , t , strike_grid [1: =1] , vols , True )

343 calls = np . i n s e r t ( c a l l s , 0, df * fwd )

344 calls = np . a p p e n d ( c a l l s , 0.0)

345 if not use_calls :

346 calls = calls = df * ( fwd = strike_grid ) # put =c a l l parity

347
348 c a l l _ p r i c e s . append ( c a l l s )

349
350 call_prices = np . a r r a y ( c a l l _ p r i c e s )

351
352 # Calculate the local vols , the drifts and the interest rates

353 probs = []

354 for j in range (0 , ntime ) :

355 dt = time_grid [ j + 1] = time_grid [ j ]

356 df = math . e x p ( =r2 * dt )

357 effectiverate = (1. = df ) / ( dt * ( theta * df + 1. = theta ) )

21

Electronic copy available at: https://ssrn.com/abstract=4194884


358 d o m r a t e s . append ( e f f e c t i v e r a t e )

359 dfdiv = math . e x p ( = r1 * dt )


360 effectivediv = (1. = dfdiv ) / ( dt * ( theta * dfdiv + 1. = theta ) )

361 d r i f t s . append ( e f f e c t i v e r a t e = effectivediv )

362
363 r = domrates [ j ]

364 mu = drifts [ j ]

365
366 c = theta * call_prices [ j + 1] + (1. = theta ) * call_prices [ j ]

367 Jc = np . matmul ( J , c)

368
369 prob = np . matmul ( L , c)

370
371 sum_prob = np . sum ( p r o b )

372 Up = np . matmul (U , prob )

373 num = (( call_prices [ j + 1] = call_prices [ j ]) / dt + r * c + mu * Jc )

374 den = (0.5 * strike_grid * strike_grid * Up )

375 # Safe division of num / den ( result is zero if den is zero )

376 lvsqr = np . d i v i d e ( num , den , o u t=np . z e r o s _ l i k e ( num ) , w h e r e=d e n != 0)

377 lvsqr [0] = 0.

378 lvsqr [n =1] = 0.

379 # In production code , local vol squares should always be floored

380 # at zero so the Thomas algorithm stays stable .

381 if floor_at_zero :

382 lvsqr [ lvsqr < 0.0] = 0.0

383 l o c a l _ v a r s . append ( l v s q r [ 0 : n ] )

384
385 local_vars = 2 #np . a r r a y ( l o c a l _ v a r s )

386 p0 . e x t e n d ( l i s t ( np . matmul ( L , call_prices [0]) ) [0: n ])

387
388 def solvePDE ( s t r i k e _ g r i d , time_grid , domrates , drifts , local_vars , p0 , payoff ,

is_upper_barrier = False ) :

389 solution = payoff

390 L = getLOperator ( s t r i k e _ g r i d )

391 S = diagonalMatrix ( strike_grid )

392 Ssqr = np . matmul ( S , S )

393 U = getUOperator ( s t r i k e _ g r i d )

394 D = getDOperator ( s t r i k e _ g r i d )

395 if is_upper_barrier :

396 D[ =1][ =1] = 0.; D[ =1][ =2] = 0. # correct boundary condition of there

is a barrier

397 I = diagonalMatrix ( [ 1 ] * len ( strike_grid ) ) # identity matrix

398 for j in r a n g e ( ntime , 0, =1) :


399 dt = time_grid [ j ] = time_grid [ j = 1]

400 r = domrates [ j = 1]
401 mu = drifts [ j = 1]
402
403 V = diagonalMatrix ( local_vars [ j = 1])

404 SsqrV = np . matmul ( S s q r , V)

405 UL = np . matmul (U , L)

406 Q = 0.5 * np . matmul ( SsqrV , UL) = r * I + mu * np . matmul ( S , D)

407 A = I = theta * dt * Q

408 B = I + (1. = theta ) * dt * Q

409 solution = np . matmul ( B , solution )

22

Electronic copy available at: https://ssrn.com/abstract=4194884


410 solution = i n v e r t A n d M u l t i p l y (A, solution )

411
412 if is_upper_barrier :

413 solution [ =1] = 0.0

414
415 pv = np . d o t ( p0 , solution )

416 return pv

417
418 if __name__ == "__main__" :

419
420 t0 = time . perf_counter ( )

421
422 # Financial parameters

423 # For demonstration purposes , we will generate ' market ' prices using a

SABR model

424 spot = 1

425 r1 = 0.05 # Dividend yield

426 r2 = 0.03 # Domestic interest rate

427 T = 1 # Time to expiry in years

428 alpha = 0.1 # SABR initial vol

429 nu = 0.5 # SABR vol =o f = v o l


430 rho = =0.5 # Spot =v o l correlation

431
432 # Solver parameters

433 nspot = 10

434 ntime = 10

435 std = 3

436 theta = 1 # 1 for fully implicit , 0.5 for semi =i m p l i c i t


437
438 # Set up simple spot / s t r i k e grid

439 # A real application would choose the boundaries more carefully taking

440 # smile into account , and would make the grid evenly spaced in log =s p o t
441 # terms and it would place strikes of importance on the grid ( see Tavella

442 # and Randall book for how to do this )

443 lower_bndy = spot * math . e x p ( =0.5 * alpha * alpha * T = alpha * std * math

. s q r t (T) )

444 upper_bndy = spot * math . e x p ( =0.5 * alpha * alpha * T + alpha * std * math

. s q r t (T) )

445 strike_grid = g e t _ g r i d ( ' random ' , l o w e r _ b n d y , upper_bndy , n s p o t ,

d e c i m a l _ p l a c e s =2)

446 time_grid = np . l i n s p a c e ( 0 , T, ntime + 1)

447
448 domrates , drifts , local_vars , p0 = [] , [] , [] , []

449 getDiscretizedParams ( strike_grid , time_grid , local_vars , drifts , domrates ,

p0 )

450
451 # Value an option

452 # Pick the strike to be one of the grid points

453 strike_index = int ( len ( strike_grid ) / 2)

454 strike = strike_grid [ strike_index ]

455 df = math . e x p ( = r 2 * T) # d i s c o u n t factor

456 fwd = math . e x p ( ( r 2 = r 1 ) * T)


457 vol = s a b r V o l s ( alpha , nu , rho , T, fwd , np . a r r a y ( [ s t r i k e ] ) )

458 true_pv = df * b l a c k S c h o l e s P V ( fwd , T, np . a r r a y ( [ s t r i k e ] ) , vol , False ) [ 0 ]

23

Electronic copy available at: https://ssrn.com/abstract=4194884


459
460 payoff = [ max ( s t r i k e = x, 0) for x in strike_grid ]

461
462 # Solve the PDE

463 pv = solvePDE ( s t r i k e _ g r i d , time_grid , domrates , drifts , local_vars , p0 ,

payoff )

464
465
466 pverror = pv = true_pv

467 t1 = time . perf_counter ( )

468
469 p r i n t ( 'PV : ' , pv )

470 p r i n t ( ' True PV : ' , true_pv )

471 print ( ' Error : ' , pverror )

472 p r i n t ( ' Time : ' , t1 = t0 )

473
474 # Now verify numerically some of the formulas in the article

475 # Note we are only printing matrices to 3 decimal places here .

476 # You can do more precise checks of course . There is an aprint function

477 # defined at the top for an alternative style of printing

478
479 # L^{ =1} as an ' inverse ' of L

480 L = getLOperator ( s t r i k e _ g r i d )

481 Lm = getLInverseOperator ( strike_grid ) # Get the inverse matrix L^{ =1}


482 l p r i n t (Lm, 3) # Print it in latex format

483
484 # Print L L^{ =1} in latex format . It is equal to diag (0 , 1, 1, . . . , 1, 0)

485 LLm = np . matmul ( L , Lm) # compute L L^{ =1}


486 l p r i n t (LLm, 3)

487
488 # Print L^{ =1} L^\ d a g g e r

489 # It is equal to diag (0 , 1, 1, . . . , 1, 0)

490 LmLdag = np . matmul (Lm, np . t r a n s p o s e ( L ) ) # Now compute L^{ =1} L^\ d a g g e r

491 a p r i n t ( LmLdag )

492
493 # Print L^{ =1} L
494 # It is * not * e q u a l to diag (0 , 1, 1, . . . , 1, 0)

495 LmL = np . matmul (Lm, L ) # compute L^{ =1} L

496 l p r i n t (LmL, 3)

497
498 # Derivation of the matrix J

499 # J is equal to =L^{ =1} D^\ d a g g e r X plus some irrelevant numbers

500 # in the first column and the last row

501 J = getJOperator ( s tri ke_ gri d )

502 lprint (J , 3)

503
504 # Here we print L^{ =1} D^\ d a g g e r X L so we can compare it to =J
505 X = diagonalMatrix ( strike_grid )

506 D = getDOperator ( s t r i k e _ g r i d )

507 LmDdagXL = np . matmul (Lm, np . matmul ( np . t r a n s p o s e (D) , np . matmul (X , L) ) )

508 l p r i n t ( LmDdagXL , 3)

509
510 # Here we print L^{ =1} D^\ d a g g e r X L = (= J ) so we can check it only has

511 # irrelevant entries in the first column and final row

24

Electronic copy available at: https://ssrn.com/abstract=4194884


512 M = LmDdagXL + J

513 l p r i n t (M, 3)

514
515 # Now value a no =t o u c h option

516 # We will set vol of vol to 0 so we can compare with the Black =S c h o l e s
value

517 spot = 1

518 r1 = 0.05 # Dividend yield

519 r2 = 0.03 # Domestic interest rate

520 T = 1 # Time to expiry in years

521 alpha = 0.1 # SABR initial vol

522 nu = 0. # SABR vol =o f = v o l


523 rho = 0. # Spot =v o l correlation

524
525 # We will use more grid points than the above demo

526 nspot = 200

527 ntime = 20

528 std = 3

529 theta = 0.5 # 1 for fully implicit , 0.5 for semi =i m p l i c i t


530
531 # Set the barrier level and calculate ture pv

532 barrier = 1.06

533 true_pv = noTouchBlackPV ( s p o t , r1 , r2 , alpha , T, barrier )

534
535 # Loop over choices of theta and ntime to check convergence of PDE against

analytic price

536 for theta in [0.5 ,1.0]:

537 for ntime in [10 , 20 , 100]:

538 lower_bndy = spot * math . e x p ( =0.5 * alpha * alpha * T = alpha *


std * math . s q r t (T) )

539 upper_bndy = barrier

540 strike_grid = get_grid ( ' l i n e a r ' , l o w e r _ b n d y , upper_bndy , n s p o t )

541 time_grid = np . l i n s p a c e ( 0 , T, ntime + 1)

542
543 domrates , drifts , local_vars , p0 = [] , [] , [] , []

544 getDiscretizedParams ( strike_grid , time_grid , local_vars , drifts ,

domrates , p0 , remove_last_point = True , f l o o r _ a t _ z e r o=T r u e )

545 strike_grid = strike_grid [ 0 : len ( strike_grid ) = 1]


546
547 # Value the no =t o u c h
548 payoff = [1.0 for x in strike_grid ]

549 payoff [ =1] = 0.0 # At the top of the barrier , we have touched the

barrier , so the payoff is zero

550
551 # Solve the PDE

552 pv = solvePDE ( s t r i k e _ g r i d , time_grid , domrates , drifts , local_vars

, p0 , payoff , i s _ u p p e r _ b a r r i e r=T r u e )

553
554 pv_error = pv = true_pv

555
556 p r i n t ( theta , ntime , pv_error )

25

Electronic copy available at: https://ssrn.com/abstract=4194884


References

Andreasen, Jesper and Brian Norsk Huge.  Finite dierence based calibration and simula-
tion. Risk July (2011).

  Volatility interpolation. Risk March (2011).

Austing, Peter.  Finite dierence schemes with exact recovery of vanilla option prices. Risk
November (2020). http://ssrn.com/abstract=3530561.

Bang, Dominique RA.  Local-Stochastic Volatility for Vanilla Modeling: A Tractable and
Arbitrage Free Approach to Option Pricing. Available at SSRN 3171877 (2019).

Bang, Dominique RA and Elias Daboussi.  Modelling of CMS-Linked Products in an RFR


framework, with Extension to Hybrids, Forward Starting and Canary Options. Forward
Starting and Canary Options (June 17, 2022) (2022).

Crank, John and Phyllis Nicolson.  A practical method for numerical evaluation of solutions
of partial dierential equations of the heat-conduction type. In: Mathematical Proceed-
ings of the Cambridge Philosophical Society. Vol. 43. 1. Cambridge University Press. 1947,
pp. 5067.

Duy, Daniel J.  A critique of the crank nicolson scheme strengths and weaknesses for
nancial instrument pricing. The Best of Wilmott (2004), p. 333.

Dupire, Bruno.  Arbitrage pricing with stochastic volatility. In: Proceedings of AFFI con-
ference. Paris, 1992.
Giles, M and R Carter.  Convergence analysis of Crank-Nicolson and Rannacher time-
marching. Technical report (2005). http://eprints.maths.ox.ac.uk/1137/1/NA-05-16.pdf.
Guyon, Julien and Pierre Henry-Labordère.  Being particular about calibration. Risk 25.1
(2012), p. 88.

Hagen, Patrick S., Deep Kumar, Andrew S Lesniewski, and Diana E Woodward.  Managing
Wilmott magazine (2002).
Smile Risk.

Haug, Espen Gaarder. The Complete Guide to Option Pricing Formulas. McGraw-Hill, 1997.

Huge, Brian.  A Note on the Fokker-Planck Equation (2018).

Rannacher, Rolf.  Finite element solution of diusion problems with irregular data. Nu-
merische Mathematik 43.2 (1984), pp. 309327.

Saporito, Yuri F, Xu Yang, and Jorge P Zubelli.  The calibration of stochastic local-volatility
models: An inverse problem perspective. Computers & Mathematics with Applications
77.12 (2019), pp. 30543067.

Tavella, D. and C. Randall. Pricing Financial Instruments: The Finite Dierence Method.
John Wiley & Sons, Inc., 2000. Chap. 5.

Thomas, Llewellyn.  Elliptic problems in linear dierential equations over a network: Watson
scientic computing laboratory. Columbia Univ., NY (1949).

26

Electronic copy available at: https://ssrn.com/abstract=4194884


Wyns, Maarten and Karel J in 't Hout.  An adjoint method for the exact calibration of
stochastic local volatility models. Journal of computational science 24 (2018), pp. 182
194.

27

Electronic copy available at: https://ssrn.com/abstract=4194884

You might also like