Skip to content

Commit ba4999f

Browse files
pawelmagnuslayoo
andauthored
new example: Asian option pricing using 2D PDE (Pawel Magnuszewski's MSc project draft, to be submitted to arXiv soon) (#543)
Co-authored-by: Sylwester Arabas <sylwester.arabas@agh.edu.pl>
1 parent 4d99b5f commit ba4999f

File tree

8 files changed

+3231
-10
lines changed

8 files changed

+3231
-10
lines changed

docs/bibliography.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,5 +186,12 @@
186186
],
187187
"label": "Smolarkiewicz and Pudykiewicz 1992 (J. Atmos. Sci. 49)",
188188
"title": "A Class of Semi-Lagrangian Approximations for Fluids"
189+
},
190+
"https://doi.org/10.1017/CBO9781139017404": {
191+
"usages": [
192+
"examples/PyMPDATA_examples/Magnuszewski_et_al_2025/monte_carlo.py"
193+
],
194+
"label": "Capiński and Zastawniak 2012 (Cambridge University Press)",
195+
"title": "Numerical Methods in Finance with C++"
189196
}
190197
}

examples/PyMPDATA_examples/Magnuszewski_et_al_2025/__init__.py

Whitespace-only changes.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
from functools import cached_property
2+
3+
import numpy as np
4+
from PyMPDATA_examples.utils.discretisation import discretised_analytical_solution
5+
6+
from PyMPDATA import ScalarField, Solver, Stepper, VectorField
7+
from PyMPDATA.boundary_conditions import Extrapolated
8+
from PyMPDATA.impl.enumerations import INNER, OUTER
9+
10+
11+
# pylint: disable=too-few-public-methods
12+
class Settings:
13+
def __init__(self, T, sgma, r, K, S_min, S_max):
14+
self.T = T
15+
self.sgma = sgma
16+
self.r = r
17+
self.K = K
18+
self.S_min = S_min
19+
self.S_max = S_max
20+
self.rh = None
21+
22+
def payoff(self, A: np.ndarray, da: np.float32 = 0, variant="call"):
23+
def call(x):
24+
return np.maximum(0, x - self.K)
25+
26+
def put(x):
27+
return np.maximum(0, self.K - x)
28+
29+
if variant == "call":
30+
payoff_func = call
31+
else:
32+
payoff_func = put
33+
34+
self.rh = np.linspace(A[0] - da / 2, A[-1] + da / 2, len(A) + 1)
35+
output = discretised_analytical_solution(self.rh, payoff_func)
36+
return output
37+
38+
39+
class Simulation:
40+
def __init__(self, settings, *, nx, ny, nt, options, variant="call"):
41+
self.nx = nx
42+
self.nt = nt
43+
self.settings = settings
44+
self.ny = ny
45+
self.dt = settings.T / self.nt
46+
log_s_min = np.log(settings.S_min)
47+
log_s_max = np.log(settings.S_max)
48+
self.S = np.exp(np.linspace(log_s_min, log_s_max, self.nx))
49+
self.A, self.dy = np.linspace(
50+
0, settings.S_max, self.ny, retstep=True, endpoint=True
51+
)
52+
self.dx = (log_s_max - log_s_min) / self.nx
53+
self.settings = settings
54+
sigma_squared = pow(settings.sgma, 2)
55+
courant_number_x = -(0.5 * sigma_squared - settings.r) * (-self.dt) / self.dx
56+
self.l2 = self.dx * self.dx / sigma_squared / self.dt
57+
self.mu_coeff = (0.5 / self.l2, 0)
58+
assert (
59+
self.l2 > 2
60+
), f"Lambda squared should be more than 2 for stability {self.l2}"
61+
self.payoff = settings.payoff(A=self.A, da=self.dy, variant=variant)
62+
stepper = Stepper(options=options, n_dims=2)
63+
x_dim_advector = np.full(
64+
(self.nx + 1, self.ny),
65+
courant_number_x,
66+
dtype=options.dtype,
67+
)
68+
cfl_condition = np.max(np.abs(self.a_dim_advector)) + np.max(
69+
np.abs(x_dim_advector)
70+
)
71+
assert cfl_condition < 1, f"CFL condition not met {cfl_condition}"
72+
self.solver = Solver(
73+
stepper=stepper,
74+
advectee=ScalarField(
75+
self.payoff_2d.astype(dtype=options.dtype)
76+
* np.exp(-self.settings.r * self.settings.T),
77+
halo=options.n_halo,
78+
boundary_conditions=self.boundary_conditions,
79+
),
80+
advector=VectorField(
81+
(x_dim_advector, self.a_dim_advector),
82+
halo=options.n_halo,
83+
boundary_conditions=self.boundary_conditions,
84+
),
85+
)
86+
self.rhs = np.zeros((self.nx, self.ny))
87+
88+
@property
89+
def payoff_2d(self):
90+
raise NotImplementedError()
91+
92+
@property
93+
def a_dim_advector(self):
94+
raise NotImplementedError()
95+
96+
@property
97+
def boundary_conditions(self):
98+
return (
99+
Extrapolated(OUTER),
100+
Extrapolated(INNER),
101+
)
102+
103+
def step(self, nt=1):
104+
self.solver.advance(n_steps=nt, mu_coeff=self.mu_coeff)
105+
106+
107+
class AsianArithmetic(Simulation):
108+
@cached_property
109+
def payoff_2d(self):
110+
return np.repeat([self.payoff], self.nx, axis=0)
111+
112+
@property
113+
def a_dim_advector(self):
114+
a_dim_advector = np.zeros((self.nx, self.ny + 1))
115+
for i in range(self.ny + 1):
116+
a_dim_advector[:, i] = -self.dt / self.dy * self.S / self.settings.T
117+
return a_dim_advector

examples/PyMPDATA_examples/Magnuszewski_et_al_2025/figs.ipynb

Lines changed: 2895 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"""
2+
This code is a Python numba-fied implementation of the Monte Carlo method
3+
for pricing Asian options taken from
4+
[Numerical Methods in Finance with C++](https://doi.org/10.1017/CBO9781139017404)
5+
"""
6+
7+
from functools import cached_property, lru_cache, partial
8+
from typing import Callable
9+
10+
import numba
11+
import numpy as np
12+
13+
jit = partial(numba.jit, fastmath=True, error_model="numpy", cache=True, nopython=True)
14+
15+
# pylint: disable=too-few-public-methods
16+
17+
18+
class BSModel:
19+
def __init__(self, S0, r, sigma, T, M, seed):
20+
self.S0 = S0
21+
self.r = r
22+
self.sigma = sigma
23+
self.sigma2 = sigma * sigma
24+
self.b = r - 0.5 * self.sigma2
25+
self.T = T
26+
self.M = M
27+
self.t = np.linspace(0, T, M)
28+
self.bt = self.b * self.t
29+
self.sqrt_tm = np.sqrt(T / M)
30+
self.seed = seed
31+
32+
@cached_property
33+
def generate_path(self):
34+
M = self.M
35+
S0 = self.S0
36+
bt = self.bt
37+
sigma = self.sigma
38+
sqrt_tm = self.sqrt_tm
39+
seed = self.seed
40+
41+
@jit
42+
def numba_seed():
43+
np.random.seed(seed)
44+
45+
if seed is not None:
46+
numba_seed()
47+
48+
@jit
49+
def body(path):
50+
path[:] = S0 * np.exp(
51+
bt + sigma * np.cumsum(np.random.standard_normal(M)) * sqrt_tm
52+
)
53+
54+
return body
55+
56+
57+
class PathDependentOption:
58+
def __init__(self, T, model, N):
59+
self.T = T
60+
self.model = model
61+
self.N = N
62+
self.payoff: Callable[[np.ndarray], float] = lambda path: 0.0
63+
64+
@cached_property
65+
def price_by_mc(self):
66+
T = self.T
67+
model_generate_path = self.model.generate_path
68+
model_r = self.model.r
69+
payoff = self.payoff
70+
M = self.model.M
71+
N = self.N
72+
73+
@jit
74+
def body():
75+
sum_ct = 0.0
76+
path = np.empty(M)
77+
for _ in range(N):
78+
model_generate_path(path)
79+
sum_ct += payoff(path)
80+
return np.exp(-model_r * T) * (sum_ct / N)
81+
82+
return body
83+
84+
85+
@lru_cache
86+
def make_payoff(K: float, option_type: str, average_type: str = "arithmetic"):
87+
assert average_type in ["arithmetic", "geometric"]
88+
if average_type != "arithmetic":
89+
raise NotImplementedError("Only arithmetic average is supported")
90+
if option_type == "call":
91+
92+
@jit
93+
def payoff(path):
94+
return max(np.mean(path) - K, 0)
95+
96+
elif option_type == "put":
97+
98+
@jit
99+
def payoff(path):
100+
return max(K - np.mean(path), 0)
101+
102+
else:
103+
raise ValueError("Invalid option")
104+
return payoff
105+
106+
107+
class FixedStrikeArithmeticAsianOption(PathDependentOption):
108+
def __init__(self, T, K, variant, model, N):
109+
super().__init__(T, model, N)
110+
self.K = K
111+
self.payoff = make_payoff(K, variant)
112+
113+
114+
class FixedStrikeGeometricAsianOption(PathDependentOption):
115+
def __init__(self, T, K, variant, model, N):
116+
super().__init__(T, model, N)
117+
self.K = K
118+
119+
if variant == "call":
120+
self.payoff = lambda path: max(np.exp(np.mean(np.log(path))) - K, 0)
121+
elif variant == "put":
122+
self.payoff = lambda path: max(K - np.exp(np.mean(np.log(path))), 0)
123+
else:
124+
raise ValueError("Invalid option type")

0 commit comments

Comments
 (0)