0% found this document useful (0 votes)
19 views

C++ Design of PDE Models

Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
19 views

C++ Design of PDE Models

Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 10

15418286, 2017, 91, Downloaded from https://onlinelibrary.wiley.com/doi/10.1002/wilm.10620 by Cochrane Netherlands, Wiley Online Library on [24/11/2022].

See the Terms and Conditions (https://onlinelibrary.wiley.com/terms-and-conditions) on Wiley Online Library for rules of use; OA articles are governed by the applicable Creative Commons License
A PDE Software Framework in C++11 for a
Class of Path-Dependent Options
Daniel J. Duffy
Datasim Finance, e-mail: [email protected]

Abstract threads and tasks as well as tasks in the Microsoft Parallel Patterns Library (PPL). We
In this article we apply the Alternating Direction Explicit (ADE) finite difference shall discuss these topics elsewhere.
method to a class of partial differential equations (PDEs) occurring in computation- We see this paper as being useful for model validators and front-office developers
al finance. We take the results of Wilmott, Lewis, and Duffy (2014) and we design a who wish to create pluggable applications in which components can be replaced by
software framework based on it using system decomposition methods in combina- other components having similar functionality. An important implicit assumption
tion with the multiparadigm language features in C++11. underlying our approach is that we design software systems with change in mind.

Keywords 2 Modeling PDEs and initial boundary value


partial differential equations, finite difference method, upwinding, ADE, C++11 problems in the functional programming style
Mathematically, a PDE is an aggregation or composition of its defining functions
such as diffusion, convection, reaction, and inhomogeneous terms. Time-dependent
1 Introduction and objectives PDEs have one time input argument and n input arguments that represent the space
In this paper we discuss how to analyze, design, and implement a C++ software dimension (in this paper we have n = 2 because we are investigating a special two-
framework for a class of partial differential equations [an exemplar of which is dis- factor PDE). Furthermore, a PDE is defined in a bounded or unbounded region of
cussed in Wilmott et al. (2014) (subsequently called WLD2014)]. The focus is on space-time with corresponding initial and boundary conditions. To this end, we
applying the author’s system decomposition techniques (Duffy, 2004), in combina- create a class that models the terms of the PDE and a class that models the domain in
tion with the new features in C++11, to produce a customizable software framework which the PDE is defined, along with its corresponding initial boundary value prob-
that reproduces the numerical results in WLD2014 and that can be extended to other lem. The first class has the interface:
kinds of PDEs and finite difference schemes. We realize a certain level of flexibility template <typename T> class TwoFactorPde
in the new framework due to the following features: { // Model a convection-diffusion-reaction PDE as a
// composition of universal function wrappers.
• Each subsystem has a single major responsibility and it has well-defined and
public:
narrow interfaces. Complex mathematical operations are hidden behind these // U_t = a11U_xx + a22U_yy + b1U_x + b2U_y + cU_xy + dU + F;
interfaces. // f = f(x,y,t) in general
• We model PDEs as compositions of universal function wrappers based on the
std::function<T (T,T,T)> a11;
functional programming model. In this way we avoid code bloat and prolifera- std::function<T (T,T,T)> a22;
tion of classes that arise when creating traditional class hierarchies based on std::function<T (T,T,T)> c;
subtype polymorphism or the Curiously Recurring Template Pattern (CRTP). std::function<T (T,T,T)> b1;
• We begin with the C++ code that we used to implement the Alternating std::function<T
std::function<T
(T,T,T)>
(T,T,T)>
b2;
d;
Direction Explicit (ADE) in WLD2014 and we port it to code that fits into the std::function<T (T,T,T)> F;
new software framework.
• Lambda functions are very useful in helping reduce code bloat, especially TwoFactorPde() = default;
};
when configuring the application. Their use promotes code readability and
maintainability. This interface supports many of the linear two-factor PDEs in computational
finance, for example basket and rainbow options, the Heston model, Asian options,
Having created the application we can then apply a range of finite difference and PDEs in fixed-income applications (Duffy, 2006):
schemes to various PDEs. We are interested in the relative accuracy of the schemes
∂ 2V ∂ 2V ∂V
and we would like to compute approximate option values in an efficient manner. We
∂V
∂t
+ 12 σ 12 S12 + (r − D1 )S1 ∂∂V + 12 σ 22 S 22 +(r − D2 )S 2 ∂∂SV + ρσ 1σ 2 S1S 2 ∂ S ∂2 S − rV = 0
∂ S12 S 1 ∂ S 22 2 1 2

can improve speedup by testing the resulting sequential code against multithreading
∂U ∂ 2U
and multitasking codes in C++ and related libraries. Specifically, we can use C++11 ∂t
+ LsU + LvU + ρσ vS ∂ S∂v
=0 (1)

48 WILMOTT magazine

48-56_WILM_Duff y_TP_Sept_2017.indd 48 08/18/2017 02:58 PM


15418286, 2017, 91, Downloaded from https://onlinelibrary.wiley.com/doi/10.1002/wilm.10620 by Cochrane Netherlands, Wiley Online Library on [24/11/2022]. See the Terms and Conditions (https://onlinelibrary.wiley.com/terms-and-conditions) on Wiley Online Library for rules of use; OA articles are governed by the applicable Creative Commons License
TECHNICAL PAPER

where 2.1 A special case: Asian-style PDEs


LS U ≡ 1 vS 2 ∂ 2U + rS ∂U − rU = 0 A special case of the two-factor models just discussed is when there is only one
2 ∂S 2 ∂S
stochastic factor, which leads to a PDE in which there is one diffusion term and no
1 2 2
∂ U ∂U
LvU ≡ 2
σ v + {κ [Θ − v(t )] − γ (S , v, t )} (2) mixed derivative. This is an example of a path-dependent option. A good example is
∂ v2 ∂v
an Asian option defined by the following PDE:
∂ 2V 2 ∂ 2V ∂ 2V
∂V
∂t
+ 12 σ 2 S 2 ∂ V
+ ρσ Sw ∂∂ + 12 w2 + rS ∂V
+ (u − wγ ) ∂∂Vr − rV = 0 ∂V
+ S ∂∂VI + 12 σ 2 S 2 + rS ∂∂VS − rV = 0
∂S 2 r ∂r2 ∂S
(3) ∂t ∂S 2
γ (r , S , t ) : Market price of risk − 1 ≤ ρ (r , S , t ) ≤ 1 : correlation. t
(4)
I ≡ ∫ S (τ )dτ .
0
These PDEs have essentially the same structure and we see that the interface can
Our interest in this paper is in the model that we have already discussed in WLD2014:
be a composition of universal function wrappers (compare with previous designs
based on subtype polymorphism and the CRTP). dA = (S–A)dt (5)
The second class models the space-time domain and boundary conditions and it leading to the PDE:
has the interface:
template <typename T> struct TwoFactorPdeDomain
∂V
∂t
= 12 σ 2 ( AS ) S 2 ∂∂S2V2 + rS ∂∂VS − rV + λ (S − A) ∂∂VA . (6)
{
// 1. Domain The parameter  is a measure of the extent of memory in the anchoring model in
Range<T> rx; WLD2014. We now discuss how to model the PDE in equation (6) and to this end we
Range<T> ry; create a new dedicated class rather than instantiating TwoFactorPde and setting
Range<T> rt;
one diffusion term and the cross-derivative term to zero. This former approach also
// 2. (Dirichlet) Boundary conditions, anticlockwise leads to readable and maintainable code at the expense of slightly less generality. The
std::function<T (T x,T t)> LowerBC; // y = yMin class is thus a special case of TwoFactorPde:
std::function<T (T x,T t)> UpperBC; // y = yMax
std::function<T (T y,T t)> LeftBC; // x = xMin template <typename T> class TwoFactorAsianPde
std::function<T (T y,T t)> RightBC; // x = xMax { // Model a convection-diffusion-reaction PDE as a composition
// of universal function wrappers. Asian-style PDE.
// 3. Initial condition
std::function<T (T x,T y)> IC; public:
}; // U_t = a11U_xx + b1U_x + b2U_y + dU + F;
// f = f(x,y,t) in general
In general, the space domain is bounded and it is the responsibility of the developer std::function<T (T,T,T)> a11;
to either truncate a quarter-plane problem to a bounded region or apply a domain std::function<T (T,T,T)> b1;
std::function<T (T,T,T)> b2;
transformation to convert an unbounded domain into a bounded one. The second
std::function<T (T,T,T)> d;
point to note is that we impose Dirichlet boundary conditions. This is a simplifica- std::function<T (T,T,T)> F;
tion because in general, boundary conditions are not necessarily of Dirichlet type.
TwoFactorAsianPde() = default;
A detailed discussion is outside the scope of this present work. Incorporating such
TwoFactorAsianPde(const TwoFactorAsianPde& pde)
boundary conditions into your schemes is 95 percent perspiration and not very diffi- : a11(pde.a11),b1(pde.b1),b2(pde.b2),d(pde.d),F(pde.F) {}
cult to do, but it does take some time. You may need to implement numerical bound- };
ary conditions in the code that implements your FD scheme.
The class TwoFactorAsianPde is the one that we will be using in the rest of this
One of the advantages of creating two separate classes lies in our ability to model
discussioan. The original form defined by equation (6) is defined on a quarter plane
many-to-many (N:N) relationships: a given PDE instance (and FD schemes that
0 < S <∞, 0 < A < ∞. As in WLD2014, we transform this domain to the unit rectangle.
approximate it) can be associated with several domains. For example, we could con-
We note that TwoFactorAsianPde is an abstract class. It cannot be instantiat-
sider the following use cases:
ed and in fact its members (which are universal function wrappers) must be assigned
• U1: A PDE that is defined on two separate domains (one using domain trunca- to concrete target methods. In this sense TwoFactorAsianPde pçlays the role of
tion while the other uses domain transformation). the abstraction in the Bridge pattern. Clients instantiate this class as objects without
• U2: A PDE that is associated with different sets of boundary conditions. the need to create classes or heavyweight class hierarchies.
• U3: Defining a single PDE instance that is shared by a domain for call options
and a domain for put options.
3 PDE preprocessing
These use cases are useful when we wish to test a new scheme and to determine We transform PDE (6) to new variables x and y defined by:
the optimal far-field condition and those boundary conditions that produce the most
S A
accurate results. It is also possible to associate multiple PDE instances with a single x= , y= , 0 < x, y < 1.
S+a A+ b
domain. The potential advantages are less code duplication and opportunities to
write multitasking code. where a and b are scaling factors which can be chosen in such a way as to allow us to
We note that the class TwoFactorPde corresponds to the abstraction define hot spot values in both original and transformed variables. For example, the
role in the Bridge pattern. Finally, we can model PDEs by creating instances of hot spot value (0.5, 0.5) in (x, y) space is usually the choice. Then the factors a and b
TwoFactorPde for all cases. For example, each of the models in equations (1), (2), are chosen in such a way that this hot spot corresponds to the user-defined hot spot
^
and (3) is mapped to an object and not a class. (S0, A0) in (S, A) space, that is a = S0, b = A0.

WILMOTT magazine 49

48-56_WILM_Duff y_TP_Sept_2017.indd 49 08/18/2017 02:58 PM


15418286, 2017, 91, Downloaded from https://onlinelibrary.wiley.com/doi/10.1002/wilm.10620 by Cochrane Netherlands, Wiley Online Library on [24/11/2022]. See the Terms and Conditions (https://onlinelibrary.wiley.com/terms-and-conditions) on Wiley Online Library for rules of use; OA articles are governed by the applicable Creative Commons License
The C++ class that models the PDE in equation (6) based on the new coordinates // Sigmoid2 function
double val = exp(-c*(log(t) - d));
becomes:
double val2 = a + (b-a)/(1.0 + val);
∂V
∂t
= 12 σ 2 ( SA ) x2 (1− x)2 ∂∂x22V + {rx(1− x) − σ 2 x2 (1− x)2 } ∂∂Vx }
return val2;

+ (γ ( S − A)(1 − y )2 / b ) ∂V
∂y
− rV
// Scale factors (hotspots)

( ).
double scale1;
ax by
0 < x < 1, 0 < y < 1 S = 1− x
, A= 1− y
(7) double scale2;

We thus see that equation (7) is a PDE defined on the unit square (0, 1) × (0, 1). // Parameters of PDE
double r;
In order to describe the resulting initial boundary value problem, we need to impose double lambda;
boundary conditions. For x = 0 and x = 1 we impose the well-known boundary con- double K;

ditions that apply to plain call or put options. For y = 0 and y = 1 no boundary condi- // Domain information
tions are needed, as mentioned in WLD2014. This fact can be verified and motivated double T;
double xMax, yMax;
by application of the Fichera theory (see Duffy, 2009; Duffy and Germani, 2013) or
by the method of characteristics. std::function<double(double S1, double S2)> payoff;
double a11(double x, double y, double t)
{
4 The anchoring PDE double tx = 1.0-x; double ty = 1.0-y;

The financial background to the current problem is given in WLD2014. We define a double S = scale1*x/tx;
double I = scale2*y/ty;
volatility function  ( ), where  = S/A and the model is:
// Straight computation, i.e. function call
dS = Sdt +  ( ) SdW. (8) double s1 = SIGMOID2(S / I);
We model the problem as one with memory. To this end, we define a variable A that
return 0.5*s1*s1*x*x*tx*tx;
represents an average or an anchoring value. One of the simplest and most tractable }
models is given by the following sigmoidal function:
A = λ ∫∞ e− λ (t−τ ) S (τ )dτ
t
(9)
double b1(double x, double y, double t)
where  is a measure of the extent of memory. {
double tx = 1.0-x; double ty = 1.0 - y;
The form of this function has been determined from time series (see WLD2014)
and it will be used as the volatility function in the PDE that is represented in equa- double S = scale1*x/tx;
tions (6) and (7). double I = scale2*y/ty;
double s1 = SIGMOID2(S / I);
Moving to C++ design of the anchoring PDE, we first note that it plays the role of return r*x*tx - s1*s1*x*x*tx;
the implementer of TwoFactorAsianPde in the Bridge pattern. It is an instance of }

TwoFactorAsianPde that is created by a Factory Method. In this case we choose double b2(double x, double y, double t)
the following alternatives: {
double tx = 1.0-x; double ty = 1.0 - y;
• A1: Defining the PDE components in a namespace.
double S = scale1*x/tx;
• A2: Creating a void function that initializes an already created reference to an double I = scale2*y/ty;
instance of TwoFactorAsianPde.
return lambda*(S - I)*ty*ty/scale2;
• A3: A Factory Method (as discussed in GOF, 1995) that creates an instance of }
std::shared_ptr<TwoFactorAsianPde<double>>.
We discuss each of these options as possible ways to create the objects that we double d(double x, double y, double t)
need. Each choice has its advantages and disadvantages. The ultimate choice depends {
return -r;
on the context. The traditional object-oriented programming style would tend to }
employ subtype polymorphism or the CRTP, both of which necessitate the creation
double F(double x, double y, double t)
of class hierarchies. The choices A1, A2, and A3 avoid this step. {
First, choice A1 involves placing all relevant data and functions in a namespace: return 0;
}
namespace AnchorPde
{ // Similar to an Asian PDE.
double IC(double x, double y)
double SIGMOID2(double t) { // Initial condition
{ // t = S/A
// Workaround/fudge to avoid spike
// Example of Table 2 if (x == 1.0)
const double a=0.6; // (vol for low S/I) x -= 0.0001;
const double b=0.15; // (vol for high S/I)
const double c=10.0; if (y == 1.0)
const double d=-0.3; // (when S=I vol is in the middle) y -= 0.0001;
const double ecd = std::exp(c*d);

50 WILMOTT magazine

48-56_WILM_Duff y_TP_Sept_2017.indd 50 08/18/2017 02:58 PM


15418286, 2017, 91, Downloaded from https://onlinelibrary.wiley.com/doi/10.1002/wilm.10620 by Cochrane Netherlands, Wiley Online Library on [24/11/2022]. See the Terms and Conditions (https://onlinelibrary.wiley.com/terms-and-conditions) on Wiley Online Library for rules of use; OA articles are governed by the applicable Creative Commons License
TECHNICAL PAPER

pde.b2 = b2;
return payoff(scale1*x/(1.0-x), scale2*y/(1.0-y));
}
pde.d = d;
pde.F = F;
// Approach: }
double BCLower(double x, double t)
{ Finally, choice A3 is an example of the Factory Method design pattern that returns a
TwoFactorAsianPde pointer. All low-level construction code is hidden behind
// V1 rough and ready
return 0; the interface. Notice that we use lambda functions to create the coefficients of the PDE:
}
std::shared_ptr<TwoFactorAsianPde<double>> CreateAsianPde()
{ // Factory method to create an Asian pde for input to FDM
double BCUpper(double x, double t) // Steps
{ // 1. Get input data
// Tricky one for variable I // 2. Create the components of the PDE
return 0; // Force the solution to be BS classic. // 3. Return the instance of AsianPde
}
double r = 0.049;
double T;
double BCLeft(double y, double t) cout << "T: "; cin >> T;
{ // y is S2 // Values where price is calculated S/I == 1
double S = 100; double I = 100;
// return 0; // C
return K* exp(-r*t); // P double K;
cout << "K: "; cin >> K;
}
double lambda = 0.5;
double BCRight(double y, double t)
{ // Define payoff as a lambda function
return 0; // P
double xMax = 1.0; // x far field
double S = scale1*xMax / (1 - xMax+0.001);
double yMax = 1.0; // y FF
return S -K*std::exp(-r*t); // C
} double xHotSpot = 0.5; double yHotSpot = 0.5;
} double scale1 = S*(1.0 - xHotSpot) / xHotSpot;
double scale2 = I*(1.0 - yHotSpot) / yHotSpot;

The above variables can be initialized in main(): TwoFactorAsianPde<double> pde;

using namespace AnchorPde; // Build components of pde


auto SIGMOID2 = [] (double t)
r = 0.049; { // t = S/A
std::cout << "T: "; std::cin >> T;
double S = 100; double I = 100; // Values where price is calculated S/I == 1 // Example of Table 2
const double a = 0.6; // (vol for low S/I)
const double b = 0.15; // (vol for high S/I)
std::cout << "K: "; std::cin >> K;
const double c = 10.0;
const double d = -0.3; // (when S=I vol is in the middle)
lambda = 0.5; const double ecd = std::exp(c*d);

// Define payoff as a lambda function // Sigmoid2 function


int cp = -1; double val = exp(-c*(log(t) - d));
if (cp > 0) // Call
payoff=[=](double S, double A)->double {return std::max(S - K, 0.0);}; double val2 = a + (b - a) / (1.0 + val);
else return val2;
payoff=[=](double S, double I)->double {return std::max(K - S, 0.0);}; };

// Domain transformation data; we use the transform z = x/(x + scale1), auto a11 = [=] (double x, double y, double t)
// w = y/(y + scale2). The hotspot (S1, S2) in (x,y) space gets mapped to {
// return 0;
// (1/2, 1/2) (usually) in (z,w) space.
double tx = 1.0 - x; double ty = 1.0 - y;
xMax = 1.0; // x far field
yMax = 1.0; // y FF double S = scale1*x / tx;
double I = scale2*y / ty;
double xHotSpot = 0.5; double yHotSpot = 0.5;
scale1 = S*(1.0 - xHotSpot)/xHotSpot; scale2 = I*(1.0 - yHotSpot)/yHotSpot; // Straight computation, i.e. function call
double s1 =SIGMOID2(S / I);

Second, choice A2 is a variation on A1 in the sense that it uses the above variables return 0.5*s1*s1*x*x*tx*tx;
};
and it encapsulates the creation process in a single function:

auto b1 = [=] (double x, double y, double t)


void CreateAnchorPde(TwoFactorAsianPde<double>& pde) {
{ double tx = 1.0 - x; double ty = 1.0 - y;

// Initialise all functions double S = scale1*x / tx;


double I = scale2*y / ty;
double s1 = SIGMOID2(S / I);
using namespace AnchorPde; return r*x*tx - s1*s1*x*x*tx;
};
pde.a11 = a11; auto b2 = [=] (double x, double y, double t)
{
// a22 = c = 0 double tx = 1.0 - x; double ty = 1.0 - y;

double S = scale1*x / tx;


^
pde.b1 = b1; double I = scale2*y / ty;

WILMOTT magazine 51

48-56_WILM_Duff y_TP_Sept_2017.indd 51 08/18/2017 02:58 PM


15418286, 2017, 91, Downloaded from https://onlinelibrary.wiley.com/doi/10.1002/wilm.10620 by Cochrane Netherlands, Wiley Online Library on [24/11/2022]. See the Terms and Conditions (https://onlinelibrary.wiley.com/terms-and-conditions) on Wiley Online Library for rules of use; OA articles are governed by the applicable Creative Commons License
return lambda*(S - I)*ty*ty / scale2; The code in this case is given by (notice that we are using the Boost Library to
};
model matrices):
auto d = [=] (double x, double y, double t)
{ namespace u = boost::numeric::ublas;
return -r; using namespace AnchorPde;
};
class TwoFactorAsianADESolver
auto F = [=] (double x, double y, double t) {
{ private:
return 0; TwoFactorPdeDomain<double> pdeDomain; // Domain, BC, IC
};
// Data structures
pde.a11 = a11; u::matrix<double> U; // upper sweep, n+1
u::matrix<double> V; // lower sweep, n+1
// a22 = c = 0
public:
pde.b1 = b1; u::matrix<double> MatNew; // averaged solution, level n+1
pde.b2 = b2; private:
pde.d = d; // Mesh-related data
pde.F = F; double hx, hy, delta_k, hx1, hy1, hx2, hy2;

return std::shared_ptr<TwoFactorAsianPde<double>>(new TwoFactorAsianPde<double>(pde)); // Mesh-point values of coefficients


} double A,B,C,D,E,F,G;
double t2,tx1,ty1;

Finally, we show the code (using design choice A1) that creates the domain in which // Other variables
double tprev, tnow, T;
the anchoring PDE is defined: std::size_t NX, NY, NT;

public:
void CreateAnchorPdeDomain(TwoFactorPdeDomain<double>& pdeDomain, std::vector<double> xmesh;
double xMax, double yMax, double T, std::vector<double> ymesh;
const std::function<double(double, double)>& IC) std::vector<double> tmesh;
{ public:
using namespace AnchorPde;
TwoFactorAsianADESolver(const TwoFactorPdeDomain<double>& domain,
pdeDomain.rx = Range<double>(0.0, xMax); const std::vector<double>& xarr,
const std::vector<double>& yarr,
pdeDomain.ry = Range<double>(0.0, yMax); const std::vector<double>& tarr)
pdeDomain.rt = Range<double>(0.0, T); : xmesh(xarr), ymesh(yarr), tmesh(tarr)
{
pdeDomain.LeftBC = BCLeft; cout << "Two-factor ADE Asian classic version\n";
pdeDomain.RightBC = BCRight; pdeDomain = domain;
pdeDomain.UpperBC = BCUpper; hx=pdeDomain.rx.spread() / static_cast<double>(xmesh.size() - 1);
pdeDomain.LowerBC = BCLower; hy=pdeDomain.ry.spread() / static_cast<double>(ymesh.size() - 1);
delta_k = pdeDomain.rt.spread()
pdeDomain.IC = IC; / static_cast<double>(tmesh.size() - 1);
} T = pdeDomain.rt.high();

// Extra, handy variables


We now discuss each of the solutions A1, A2, and A3 as input to the ADE scheme. hx1 = 1.0/hx;
hx2 = 1.0/(hx*hx);
To summarize, each choice creates a PDE that ADE uses. hy1 = 1.0/hy;
hy2 = 1.0/(hy*hy);

// Some optimising variables

5 ADE for anchoring PDE t2 = delta_k * hx2;


tx1 = delta_k * hx1;
ty1 = delta_k * hy1;
The ADE method is well documented. For more background information, see
// Initialise U, UOld, V, VOld, MatNew data structures
Saul’yev (1964), Campbell and Yin (2006), Duffy (2009), Pealat and Duffy (2011), // NumericMatrix(I rows, I columns, I rowStart, I columnStart);
// Constructor with size & start index
Duffy and Germani (2013), and Buchova et al. (2015). In the current case we apply U = u::matrix<double>(xmesh.size(), ymesh.size());
V = u::matrix<double>(xmesh.size(), ymesh.size());
both the Barakat–Clark and Saul’yev variants of ADE to the x factor in equation (7)
while we use implicit upwinding for the y factor in equation (7). MatNew = u::matrix<double>(xmesh.size(), ymesh.size());

We focus on the Barakat–Clark ADE variant with design choice A1. The cor- }
initIC();

responding class is called TwoFactorAsianADESolver and it has the following


~TwoFactorAsianADESolver()
member functions: {

}
• Constructor: initialize mesh arrays and matrices holding option prices.
///////////////////////////////////////////////////////////////////
• calculate(): compute the left-to-right and right-to-left sweeps and their void initIC()
{ // Utility function to initialise payoff function and BCs at t = 0
average at each time level.
• calculateBC(): calculate the boundary conditions on each of the four tprev = tnow = tmesh[0];

boundaries of the domain of integration at each time level. // Now initialise values in interior of interval using
// the initial function 'IC' from the PDE
• initIC(): initialize the solution at t = 0 by assigning it to the discretized val- for (std::size_t i = 0; i < xmesh.size(); ++i)
{
ues at the mesh points of the payoff function. for (std::size_t j = 0; j < ymesh.size(); ++j)
• result(): this is the implementation of what is essentially the state machine {
MatNew(i,j) = U(i, j) = V(i, j) = AnchorPde::IC(xmesh[i], ymesh[j]);
that marches the solution from payoff up to expiration. In a later version of the }
}
software it will be a member function of a mediator class, as already discussed }
in our work.

52 WILMOTT magazine

48-56_WILM_Duff y_TP_Sept_2017.indd 52 08/18/2017 02:58 PM


15418286, 2017, 91, Downloaded from https://onlinelibrary.wiley.com/doi/10.1002/wilm.10620 by Cochrane Netherlands, Wiley Online Library on [24/11/2022]. See the Terms and Conditions (https://onlinelibrary.wiley.com/terms-and-conditions) on Wiley Online Library for rules of use; OA articles are governed by the applicable Creative Commons License
TECHNICAL PAPER

void calculateBC() cout << "result";


{ // Calculate the discrete BC on the FOUR edges of the boundary
for (std::size_t n = 1; n < tmesh.size(); ++n)
// Lower and Upper BC {
std::size_t lower = 0; if ((n/100)*100 == n)
std::size_t upper = ymesh.size()-1; {
for (std::size_t i = 0; i < xmesh.size(); ++i) cout << n << ", ";
{ }
MatNew(i, lower) = U(i, lower) = V(i, lower)
= AnchorPde::BCLower(xmesh[i], tnow); // N/A anymore tnow = tmesh[n];
MatNew(i, upper) = U(i, upper) = V(i, upper)
= AnchorPde::BCUpper(xmesh[i], tnow); calculateBC(); // Calculate the BC at n+1
} calculate (); // Calculate the solution at n+1

tprev = tnow;
// Left and Right BC }
std::size_t left = 0;
std::size_t right = xmesh.size() -1; }
for (std::size_t j = 0; j < ymesh.size(); ++j)
{
MatNew(left, j) = U(left, j) = V(left, j) = AnchorPde::BCLeft(ymesh[j], tnow); An example of use is:
MatNew(right, j) = U(right, j) = V(right, j) = AnchorPde::BCRight(ymesh[j], tnow);
}
} // Domain
TwoFactorPdeDomain<double> pdeDomain;
CreateAnchorPdeDomain(pdeDomain, xMax, yMax, T, payoff);
void calculate()
{ // Tells how to calculate sol. at n+1, Explicit ADE schemes
// Meshes. Create the mesh
long NX = 100; long NY = 100; long NT = 100;
double mx, my;
int sgn; cout << "Give NX \n"; cin >> NX;
cout << "Give NY \n"; cin >> NY;
cout << "Give NT \n"; cin >> NT;
for (std::size_t i = 1; i <= xmesh.size()-2; ++i)
{
mx = xmesh[i]; std::vector<double> xmesh = pdeDomain.rx.mesh(NX);
for (std::size_t j = 1; j <= ymesh.size()-2; ++j) std::vector<double> ymesh = pdeDomain.ry.mesh(NY);
{
std::vector<double>tmesh = pdeDomain.rt.mesh(NT);
// Create coefficients
// hU_t = aU_xx + bU_yy + cU_xy + dU_x + eU_y + fU + G; // f = f(x,y,t) cout << "Now creating solver\n";
my = ymesh[j];

A = AnchorPde::a11(mx, my, tnow) * t2;


TwoFactorAsianADESolver solver(pdeDomain, xmesh, ymesh, tmesh);
D = 0.5*AnchorPde::b1(mx, my, tnow) * tx1; solver.result();
double sgnD = MySign(D);
E = AnchorPde::b2(mx, my, tnow) * ty1;
sgn = MySign(E); Then we can access the option values in solver.MatNew, for example, by display-
F = AnchorPde::d(mx, my, tnow) * delta_k;
G = AnchorPde::F(mx, my, tnow) * delta_k; ing them in Excel, for example.
// Larkin + 1st order upwind in y
U(i, j) = (U(i,j)*(1.0-A) + U(i+1,j)*(A+D)
+ sgn*E*U(i,j+sgn) + U(i-1,j)*(A-D) + G)
/ (1.0+A-F+sgn*E);
5.1 The Saul’yev method and Factory Method pattern
} We now discuss how we applied choice A3 as mentioned in Section 4 to implement
}
the original ADE method as developed in Saul’yev (1964). In this case we create a
for (std::size_t i = xmesh.size()-2; i >= 1 ; --i) class that implements this method and that is composed of both a domain and a
{
mx = xmesh[i]; PDE that models equation (7). This design is more maintainable than the designs
for (std::size_t j = ymesh.size() - 2; j >= 1; --j)
{
based on choices A1 and A2, because now the classes have tight internal cohesion
// Create coefficients
my = ymesh[j];
and are loosely coupled with other classes. Furthermore, each class has a single major
A = AnchorPde::a11(mx, my, tnow) * t2;
responsibility.
D = 0.5*AnchorPde::b1(mx, my, tnow) * tx1; The class that we now discuss is almost the same as that in Section 5, except that
E = AnchorPde::b2(mx, my, tnow) * ty1;
sgn = MySign(E); in addition it contains an embedded PDE instance:
F = AnchorPde::d(mx, my, tnow) * delta_k;
G = AnchorPde::F(mx, my, tnow) * delta_k;
class SaulyevTwoFactorAsianADESolverVersion2
// Larkin + 1st order upwind {
V(i, j) = (V(i, j)*(1.0 - A) + V(i - 1, j)*(A - D) private:
+ V(i+1, j)*(A + D) + sgn*E*V(i, j + sgn) + G) TwoFactorPdeDomain<double> pdeDomain; // Domain, BC, IC
/ (1.0 + A - F + sgn*E); std::shared_ptr<TwoFactorAsianPde<double>> pde; // the new extra member
}
// ETC. as in section 5
} };

The components of this class are initialised through its constructor:


for (std::size_t i = 0; i < MatNew.size1() ; ++i) SaulyevTwoFactorAsianADESolverVersion2
{ (const std::shared_ptr<TwoFactorAsianPde<double>>& asianPde,
for (std::size_t j = 0; j < MatNew.size2(); ++j) const TwoFactorPdeDomain<double>& domain,
{ const std::vector<double>& xarr, const std::vector<double>& yarr,
MatNew(i,j) = 0.5 * (U(i,j) + V(i,j));
const std::vector<double>& tarr)
U(i, j) = MatNew(i, j);
V(i, j) = MatNew(i, j);
{
} // body
} }
}

Since we have different ways to create a PDE, we encapsulate each choice in a fac-
void result()
{ // The result of the calculation tory method whose sole task is to create a PDE instance based on input from a given
^

WILMOTT magazine 53

48-56_WILM_Duff y_TP_Sept_2017.indd 53 08/18/2017 02:58 PM


15418286, 2017, 91, Downloaded from https://onlinelibrary.wiley.com/doi/10.1002/wilm.10620 by Cochrane Netherlands, Wiley Online Library on [24/11/2022]. See the Terms and Conditions (https://onlinelibrary.wiley.com/terms-and-conditions) on Wiley Online Library for rules of use; OA articles are governed by the applicable Creative Commons License
source. To this end we consider a number of factories, one of which is (we comment 1. Create an abstract PDE class consisting of universal function wrappers.
out and remove that code which has already been introduced in Section 4): 2. Create a (finite difference) FD class with an embedded pointer to an instance
of the PDE class as well as an embedded domain object.
std::shared_ptr<TwoFactorAsianPde<double>> CreateAsianPde(double K, double T) 3. Create a factory method to instantiate the PDE class in step 1 and supply the
{ // Factory method to create an Asian pde for input to FDM newly created object in the constructor of the FD class in step 2.
// Steps
// 1. Get input data In short, we use composition (HAS-A relationship) instead of the more pervasive
// 2. Create the components of the PDE
// 3. Return the instance of AsianPde IS-A relationship that characterizes subtype polymorphism and class hierarchies.
For example, it would be possible to implement the Barakat–Clark variant of ADE
double r = 0.049;
based on this design in C++. This would be a useful exercise to execute.
// Values where price is calculated S/I == 1
double S = 100; double I = 100;

double lambda = 0.5;


6 Useful utilities
We have used domain transformation to map (S, A) space to (x, y) space as discussed
// Define payoff as a lambda function
double xMax = 1.0; // x far field in Section 3. We then compute option prices by approximating the PDE in (x, y)
double yMax = 1.0; // y FF space by a finite difference scheme. The resulting data structure is a matrix of dis-
double xHotSpot = 0.5; double yHotSpot = 0.5; crete values in (x, y) space at expiration t =T. One specific scenario is to compute the
double scale1 = S*(1.0 - xHotSpot) / xHotSpot; option price at the hot spot (0. 5, 0. 5) in (x, y) space and then determine what this
double scale2 = I*(1.0 - yHotSpot) / yHotSpot;
price is in (S, A) space. In this case we are interested in the specific values:
TwoFactorAsianPde<double> pde;
double S = 100; double I = 100;
// Build components of pde
auto SIGMOID2 = [] (double t)
{ // t = S/A But the question now is to determine in which cell of the matrix of option values to
get the option value! To answer this question, we compute the pair of indices in the
//
matrix corresponding to the hot spots and then we use these indices in the matrix of
};
option values to locate the price. A typical example is:
auto a11 = [=] (double x, double y, double t)
{ auto values = FindMeshValues
// (solver.xmesh, solver.ymesh, xHotSpot, yHotSpot);
}; std::cout << "MaxA, MaxB: " << std::get<0>(values)
<< ", " << std::get<1>(values) << endl;
auto b1 = [=] (double x, double y, double t)
{
//
We do not develop home-grown code to search in arrays, as this functional-
}; ity already exists in STL. To this end, we embed std::upper_bound in
auto b2 = [=] (double x, double y, double t) FindMeshValues:
{
std::tuple<std::size_t, std::size_t >
//
FindMeshValues(const std::vector<double>& xarr,
}; const std::vector<double>& yarr,
double x, double y)
{ // Compute indices by searching in an array xarr for a 'threshold' value x.
auto d = [=] (double x, double y, double t)
{ // Find position of first element in vector that satisfies
// // the predicate d >= x.
}; // Logarithmic complexity for random-access iterators
auto posA = std::upper_bound(xarr.begin(), xarr.end(), x);
auto F= [=] (double x, double y, double t) auto posB = std::upper_bound(yarr.begin(), yarr.end(), y);
{
// auto maxA = std::distance(xarr.begin(), posA);
}; auto maxB = std::distance(yarr.begin(), posB);

pde.a11 = a11; return std::make_tuple(maxA, maxB);


}
// a22 = c = 0

pde.b1 = b1; This is efficient code, because the mesh arrays support random-access iterators. For
pde.b2 = b2; cases in which the mesh arrays do not support random-access iterators, we can use
pde.d = d;
pde.F = F; std::find_if but it is slower:
return std::shared_ptr<TwoFactorAsianPde<double>> // Find position of 1st element in vector that satisfies the predicate d >= x.
(new TwoFactorAsianPde<double>(pde)); // Linear complexity.
} auto posA = std::find_if(xarr.begin(), xarr.end(), [&](double d)
{ return d >= x; });
auto posB = std::find_if(yarr.begin(), yarr.end(), [&](double d)
This design can be applied to a wide range of problems in which PDEs are approxi- { return d >= y; });

mated by finite difference methods. It is a combination of a structural pattern and a In general, it is recommended to investigate the possibility of using STL algorithms
creational pattern, namely Adapter and Factory Method, respectively (as discussed as building blocks when creating higher-level functionality. This avoids reinvention
in GOF, 1995). A summary of the design steps is: of the software wheel.

54 WILMOTT magazine

48-56_WILM_Duff y_TP_Sept_2017.indd 54 08/18/2017 02:58 PM


15418286, 2017, 91, Downloaded from https://onlinelibrary.wiley.com/doi/10.1002/wilm.10620 by Cochrane Netherlands, Wiley Online Library on [24/11/2022]. See the Terms and Conditions (https://onlinelibrary.wiley.com/terms-and-conditions) on Wiley Online Library for rules of use; OA articles are governed by the applicable Creative Commons License
TECHNICAL PAPER

7 Accuracy and performance We automate the process of checking the computed values from Table 2 of
We know that the Saul’yev scheme is unconditionally stable and that it is first-order WLD2014. To this end, we first create two arrays representing expirations and
accurate (Saul’yev, 1964). This is in contrast to the FTCS (Forward in Time Centered strikes:
in Space) scheme that is also first-order accurate but only conditionally stable, which
// Input data to the option pricer
demands that we choose a large number of time steps in order to avoid oscillations. std::vector<double> expiries = { 0.25, 1.0, 3.0, 10.0 };
Furthermore, we use upwinding or downwinding in the y variable in equation (7), std::vector<double> strikes = { 60.0, 80.0, 100.0, 120.0, 140.0 };
depending on the sign of the convection coefficient. This method can be applied to
We also need to define some extra variables:
the model problem:
// Values where price is calculated S/I == 1
∂u ∂u double S = 100; double I = 100;
+ a = 0, −∞ < x < ∞. t > 0. (11)
∂t ∂x double xMax = 1.0; // x far field
double yMax = 1.0; // y FF

The simplest upwind scheme is the first-order upwind scheme, defined by: double xHotSpot = 0.5; double yHotSpot = 0.5;
double scale1 = S*(1.0 - xHotSpot) / xHotSpot;
double scale2 = I*(1.0 - yHotSpot) / yHotSpot;
u nj +1−u nj u nj −u nj−1
∆t
+a h
= 0 for a > 0 (12) long NX = 500; long NY = 500; long NT = 2000;
std::cout << "NX, NY, NT (put Saul’yev): "
<< NX << "," << NY << "," << NT << '\n';

and: TwoFactorPdeDomain<double> pdeDomain;


u nj +1−u nj u nj+1−u nj CreateAnchorPdeDomain(pdeDomain, xMax, yMax, expiries[t], payoff);

∆t
+a h
= 0 for a < 0. (13) std::vector<double> xmesh = pdeDomain.rx.mesh(NX);
std::vector<double> ymesh = pdeDomain.ry.mesh(NY);
std::vector<double>tmesh = pdeDomain.rt.mesh(NT);

It can be shown using von Neumann stability analysis (see Duffy, 2006) that this
scheme is stable if the famous Courant–Friedrichs–Lewy (CFL) condition is satisfied: The following double loop computes the option price for each expiration
and strike:
a ∆t
c =| |≤ 1 (14)
h for (std::size_t k = 0; k < strikes.size(); ++k)
{
// Define payoff as a lambda function
Schemes (12) and (13) are first-order accurate in space and time and they can intro- int cp = -1;
if (cp > 0)
duce severe numerical diffusion and dissipation in the solution due to the presence payoff = [=](double S, double A) -> double
{ return std::max(S - strikes[k], 0.0); }; // call
of large gradients. In our work here we take a modified version of schemes (12) and else
payoff = [=](double S, double I) -> double
(13), namely: { return std::max(strikes[k] - S, 0.0); }; // put

u nj +1−u nj u nj +1−u nj−1 for (std::size_t t = 0; t < expiries.size(); ++t)


∆t
+a h
= 0 for a > 0 (15) {
std::cout<<"T: "<<expiries[t]<<", K: "<<strikes[k] << ", ";

auto pde = CreateAsianPde(strikes[k], expiries[t]);


and: SaulyevTwoFactorAsianADESolverVersion2 solver(pde, pdeDomain, xmesh, ymesh, tmesh);
solver.result();
unj +1−unj u nj+1−u nj +1
∆t
+a h
= 0 for a < 0. (16) // Postprocessing: index ranges to analyse the error.
auto values = FindMeshValues (solver.xmesh, solver.ymesh, xHotSpot, yHotSpot);
cout << "price at hot spot: "
<< solver.MatNew (std::get<0>(values), std::get<1>(values));
}
Schemes (15) and (16) could informally be called semi-implicit schemes. std::cout << '\n';
}
We can verify (again by von Neumann stability analysis) that this scheme is
unconditionally stable and hence we can ignore the constraint (14), which is highly
The output from this code is shown in Table 1. The results are similar to those in
beneficial. We incorporate schemes (15) and (16) into our C++ code in Section 5 (see
WLD2014.
member function calculate()).
We now test the accuracy of the Saul’yev scheme by taking the numerical
examples in Table 2 of WLD2014 and attempting to reproduce them. We note that 8 Summary and conclusions
the results in that table were computed using the Mathematica NDSolve package. In this paper we have applied the ADE method (Barakat–Clark and Saul’yev vari-
In general, second (or higher)-order divided differencing is applied to the spatial ants) to model a special kind of path-dependent option with one stochastic factor
derivatives, resulting in a system of ordinary differential equations (ODEs), which is and one deterministic factor. The resulting PDE is similar to that used when pricing
then solved in time by an adaptive high-order integrator such as the Bulisrch–Stoer Asian options. We were able to reproduce the results in WLD2014 that were com-
method, for example. In WLD2014 the mesh sizes NX = NY = 250 are specified. In puted using the NDSolve package in the Mathematica language. We have seen that
our tests on ADE we take NX = NY = 500, NT = 2000, which makes a direct compari- even with a moderate number of mesh points (for example, in the range [200, 400]),
son between the methods somewhat difficult in a sense. However, we can get two- or we can achieve two- to three-digit accuracy up to 3 years’ expiration and beyond.
three-digit accuracy. In general, the larger NT is, the better the accuracy of ADE ADE is one of the most efficient schemes (see Pealat and Duffy, 2011) and a possible
^
matches that delivered by NDSolve. project might be a feasibility study for calibration.

WILMOTT magazine 55

48-56_WILM_Duff y_TP_Sept_2017.indd 55 08/18/2017 02:58 PM


15418286, 2017, 91, Downloaded from https://onlinelibrary.wiley.com/doi/10.1002/wilm.10620 by Cochrane Netherlands, Wiley Online Library on [24/11/2022]. See the Terms and Conditions (https://onlinelibrary.wiley.com/terms-and-conditions) on Wiley Online Library for rules of use; OA articles are governed by the applicable Creative Commons License
Table 1: Computed option values from the Saul’yev ADE scheme Some lessons learned were: (1) applying ADE to path-dependent options; (2) an
analysis of numerical schemes for first-order hyperbolic PDEs; and (3) using a func-
NX, NY, NT (put Saul’yev): 500,500,2000
tional programming style in C++ to design a stable software framework for the class
T: 0.25, K: 60, price at hot spot: 0.000439322
T: 1, K: 60, price at hot spot: 0.169892 of problems in this paper.
T: 3, K: 60, price at hot spot: 0.968049
T: 10, K: 60, price at hot spot: 1.94742 Daniel J. Duffy is founder of Datasim Financial and has been involved with C++ and
its applications since 1989. More recently he has been involved with the design of
T: 0.25, K: 80, price at hot spot: 0.0431205 computational finance applications in C++11. He has a PhD in numerical analysis
T: 1, K: 80, price at hot spot: 0.850115 from Trinity College (Dublin University) and can be contacted at [email protected].
T: 3, K: 80, price at hot spot: 2.3882
T: 10, K: 80, price at hot spot: 3.57465

T: 0.25, K: 100, price at hot spot: 2.83833


T: 1, K: 100, price at hot spot: 4.6868 References
T: 3, K: 100, price at hot spot: 6.05442 Buchova, Z., Ehrhardt, M., and Guenther, M. 2015. Alternating direction explicit meth-
T: 10, K: 100, price at hot spot: 6.06422
ods for convection diffusion equation. Acta Mathematica Universitatis Comenianae
T: 0.25, K: 120, price at hot spot: 18.5937 LXXXIV(2), 309–325.
T: 1, K: 120, price at hot spot: 16.2815 Campbell, L.J. and Yin, B. 2006. On the Stability of Alternating-Direction Explicit Methods
T: 3, K: 120, price at hot spot: 13.7188 for Advection-Diffusion Equations. New York: Wiley Interscience.
T: 10, K: 120, price at hot spot: 9.68991 Duffy, D.J. 2004. Domain Architectures. Chichester: John Wiley & Sons, Ltd.
Duffy, D.J. 2006. Finite Difference Methods in Financial Engineering. Chichester: John
T: 0.25, K: 140, price at hot spot: 38.2958 Wiley & Sons, Ltd.
T: 1, K: 140, price at hot spot: 33.5593 Duffy, D. 2009. Unconditionally stable and second-order accurate explicit finite dif-
T: 3, K: 140, price at hot spot: 25.4875 ference schemes using domain transformation. Part I. One-factor equity problems.
T: 10, K: 140, price at hot spot: 14.6057 Available online at: ssrn.com/abstract=1552926.
Duffy, D. and Germani, A. 2013. C# for Financial Markets. Chichester: John Wiley & Sons, Ltd.
Gamma, E., Helm, R., Johnson, R., and Vlissides, J. (GOF). 1995. Design Patterns. New
From a numerical analysis perspective, we have paid some attention to the York: Addison-Wesley.
problem of choosing a stable and accurate finite difference scheme to approximate Pealat, G. and Duffy, D. 2011. The alternating direction explicit (ADE) method for one-
the first-order hyperbolic part of the PDE defined by equation (7). In particular, we factor problems. Wilmott Magazine, July.
showed how to avoid spurious reflection at upstream and downstream boundaries Saul’yev, V.K. 1964. Integration of Equations of Parabolic Type by the Method of Nets. New
caused by using three-point second-order approximations to the first-order deriva- York: Pergamon Press.
tive with respect to the averaged variable A (or y after we perform domain transfor- Wilmott, P., Lewis, A., and Duffy, D. 2014. Modelling volatility and valuing derivatives
mation). Many quants have learned this the hard way as well. under anchoring. Wilmott Magazine 73, 48–57.

56 WILMOTT magazine

48-56_WILM_Duff y_TP_Sept_2017.indd 56 08/18/2017 02:58 PM


15418286, 2017, 91, Downloaded from https://onlinelibrary.wiley.com/doi/10.1002/wilm.10620 by Cochrane Netherlands, Wiley Online Library on [24/11/2022]. See the Terms and Conditions (https://onlinelibrary.wiley.com/terms-and-conditions) on Wiley Online Library for rules of use; OA articles are governed by the applicable Creative Commons License

You might also like