PyMPDATA_examples.Magnuszewski_et_al_2025.monte_carlo

This code is a Python numba-fied implementation of the Monte Carlo method for pricing Asian options taken from Numerical Methods in Finance with C++

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