optPricing 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- optpricing-2.0.0.dist-info/METADATA +105 -0
- optpricing-2.0.0.dist-info/RECORD +92 -0
- optpricing-2.0.0.dist-info/WHEEL +5 -0
- optpricing-2.0.0.dist-info/entry_points.txt +3 -0
- optpricing-2.0.0.dist-info/licenses/LICENSE +9 -0
- optpricing-2.0.0.dist-info/top_level.txt +1 -0
- quantfin/__init__.py +101 -0
- quantfin/atoms/__init__.py +21 -0
- quantfin/atoms/bond.py +28 -0
- quantfin/atoms/option.py +70 -0
- quantfin/atoms/rate.py +46 -0
- quantfin/atoms/stock.py +39 -0
- quantfin/calibration/__init__.py +25 -0
- quantfin/calibration/calibrator.py +155 -0
- quantfin/calibration/fit_jump_parameters.py +66 -0
- quantfin/calibration/fit_market_params.py +120 -0
- quantfin/calibration/iv_surface.py +155 -0
- quantfin/calibration/technique_selector.py +47 -0
- quantfin/calibration/vectorized_bsm_iv.py +93 -0
- quantfin/calibration/vectorized_integration_iv.py +143 -0
- quantfin/cli.py +372 -0
- quantfin/config.py +57 -0
- quantfin/dashboard/__init__.py +14 -0
- quantfin/dashboard/app.py +26 -0
- quantfin/dashboard/pages/1_Calibration.py +57 -0
- quantfin/dashboard/pages/2_Pricer_and_Greeks.py +150 -0
- quantfin/dashboard/pages/3_Financial_Tools.py +69 -0
- quantfin/dashboard/pages/__init__.py +0 -0
- quantfin/dashboard/plots.py +157 -0
- quantfin/dashboard/service.py +176 -0
- quantfin/dashboard/widgets.py +87 -0
- quantfin/data/__init__.py +26 -0
- quantfin/data/historical_manager.py +86 -0
- quantfin/data/market_data_manager.py +236 -0
- quantfin/models/__init__.py +54 -0
- quantfin/models/base/__init__.py +16 -0
- quantfin/models/base/base_model.py +198 -0
- quantfin/models/base/validators.py +95 -0
- quantfin/models/bates.py +192 -0
- quantfin/models/blacks_approx.py +134 -0
- quantfin/models/bsm.py +262 -0
- quantfin/models/cev.py +109 -0
- quantfin/models/cgmy.py +189 -0
- quantfin/models/cir.py +90 -0
- quantfin/models/dupire_local.py +67 -0
- quantfin/models/heston.py +204 -0
- quantfin/models/hyperbolic.py +170 -0
- quantfin/models/kou.py +148 -0
- quantfin/models/merton_jump.py +225 -0
- quantfin/models/nig.py +180 -0
- quantfin/models/perpetual_put.py +115 -0
- quantfin/models/sabr.py +64 -0
- quantfin/models/sabr_jump.py +68 -0
- quantfin/models/vasicek.py +88 -0
- quantfin/models/vg.py +178 -0
- quantfin/parity/__init__.py +13 -0
- quantfin/parity/implied_rate.py +110 -0
- quantfin/parity/parity_model.py +178 -0
- quantfin/techniques/__init__.py +31 -0
- quantfin/techniques/base/__init__.py +21 -0
- quantfin/techniques/base/base_technique.py +54 -0
- quantfin/techniques/base/greek_mixin.py +265 -0
- quantfin/techniques/base/iv_mixin.py +116 -0
- quantfin/techniques/base/lattice_technique.py +129 -0
- quantfin/techniques/base/pricing_result.py +27 -0
- quantfin/techniques/base/random_utils.py +31 -0
- quantfin/techniques/closed_form.py +296 -0
- quantfin/techniques/crr.py +59 -0
- quantfin/techniques/fft.py +178 -0
- quantfin/techniques/integration.py +202 -0
- quantfin/techniques/kernels/__init__.py +37 -0
- quantfin/techniques/kernels/lattice_kernels.py +286 -0
- quantfin/techniques/kernels/mc_kernels.py +337 -0
- quantfin/techniques/leisen_reimer.py +59 -0
- quantfin/techniques/monte_carlo.py +259 -0
- quantfin/techniques/pde.py +155 -0
- quantfin/techniques/topm.py +59 -0
- quantfin/workflows/__init__.py +14 -0
- quantfin/workflows/backtest_workflow.py +133 -0
- quantfin/workflows/configs/__init__.py +51 -0
- quantfin/workflows/configs/bates_config.py +25 -0
- quantfin/workflows/configs/bsm_config.py +9 -0
- quantfin/workflows/configs/cev_config.py +16 -0
- quantfin/workflows/configs/cgmy_config.py +18 -0
- quantfin/workflows/configs/heston_config.py +21 -0
- quantfin/workflows/configs/hyperbolic_config.py +18 -0
- quantfin/workflows/configs/kou_config.py +16 -0
- quantfin/workflows/configs/merton_config.py +18 -0
- quantfin/workflows/configs/nig_config.py +13 -0
- quantfin/workflows/configs/sabr_config.py +19 -0
- quantfin/workflows/configs/vg_config.py +9 -0
- quantfin/workflows/daily_workflow.py +150 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from scipy.special import kv
|
|
8
|
+
from scipy.stats import genhyperbolic
|
|
9
|
+
|
|
10
|
+
from quantfin.models.base import CF, BaseModel, ParamValidator
|
|
11
|
+
|
|
12
|
+
__doc__ = """
|
|
13
|
+
Defines the Hyperbolic pure-jump Lévy model.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HyperbolicModel(BaseModel):
|
|
18
|
+
"""Hyperbolic pure-jump Lévy model."""
|
|
19
|
+
|
|
20
|
+
name: str = "Hyperbolic"
|
|
21
|
+
supports_cf: bool = True
|
|
22
|
+
is_pure_levy: bool = True
|
|
23
|
+
default_params = {"alpha": 15.0, "beta": -5.0, "delta": 0.5, "mu": 0.0}
|
|
24
|
+
|
|
25
|
+
def __init__(self, params: dict[str, float] | None = None):
|
|
26
|
+
"""
|
|
27
|
+
Initializes the Hyperbolic model.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
params : dict[str, float] | None, optional
|
|
32
|
+
A dictionary of model parameters. If None, `default_params` are used.
|
|
33
|
+
Defaults to None.
|
|
34
|
+
"""
|
|
35
|
+
super().__init__(params or self.default_params)
|
|
36
|
+
|
|
37
|
+
def _validate_params(self) -> None:
|
|
38
|
+
p = self.params
|
|
39
|
+
req = ["alpha", "beta", "delta", "mu"]
|
|
40
|
+
ParamValidator.require(p, req, model=self.name)
|
|
41
|
+
ParamValidator.positive(p, ["alpha", "delta"], model=self.name)
|
|
42
|
+
if not (abs(p["beta"]) < p["alpha"]):
|
|
43
|
+
raise ValueError("Hyperbolic params must satisfy |beta| < alpha.")
|
|
44
|
+
|
|
45
|
+
def __eq__(self, other: object) -> bool:
|
|
46
|
+
if not isinstance(other, HyperbolicModel):
|
|
47
|
+
return NotImplemented
|
|
48
|
+
return self.params == other.params
|
|
49
|
+
|
|
50
|
+
def __hash__(self) -> int:
|
|
51
|
+
return hash((self.__class__, tuple(sorted(self.params.items()))))
|
|
52
|
+
|
|
53
|
+
def _cf_impl(
|
|
54
|
+
self,
|
|
55
|
+
*,
|
|
56
|
+
t: float,
|
|
57
|
+
spot: float,
|
|
58
|
+
r: float,
|
|
59
|
+
q: float,
|
|
60
|
+
**_: Any,
|
|
61
|
+
) -> CF:
|
|
62
|
+
"""
|
|
63
|
+
Risk-neutral characteristic function for the log-spot price log(S_t).
|
|
64
|
+
|
|
65
|
+
This is the risk-neutral characteristic function, which includes the
|
|
66
|
+
drift adjustment.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
t : float
|
|
71
|
+
The time to maturity of the option, in years.
|
|
72
|
+
spot : float
|
|
73
|
+
The current price of the underlying asset.
|
|
74
|
+
r : float
|
|
75
|
+
The continuously compounded risk-free rate.
|
|
76
|
+
q : float
|
|
77
|
+
The continuously compounded dividend yield.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
CF
|
|
82
|
+
The characteristic function.
|
|
83
|
+
"""
|
|
84
|
+
p = self.params
|
|
85
|
+
alpha, beta, delta, mu = p["alpha"], p["beta"], p["delta"], p["mu"]
|
|
86
|
+
compensator = np.log(self.raw_cf(t=1.0)(-1j)) # E[exp(X_1)] = phi_raw(-i)
|
|
87
|
+
drift = r - q - compensator
|
|
88
|
+
|
|
89
|
+
def phi(u: np.ndarray | complex):
|
|
90
|
+
return self.raw_cf(t=t)(u) * np.exp(1j * u * (np.log(spot) + drift * t))
|
|
91
|
+
|
|
92
|
+
return phi
|
|
93
|
+
|
|
94
|
+
def raw_cf(self, *, t: float) -> Callable:
|
|
95
|
+
"""
|
|
96
|
+
Returns the CF of the raw Hyperbolic process, without drift or spot.
|
|
97
|
+
|
|
98
|
+
This function represents the characteristic function of the process
|
|
99
|
+
before the risk-neutral drift adjustment is applied.
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
t : float
|
|
104
|
+
The time horizon.
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
Callable
|
|
109
|
+
The raw characteristic function.
|
|
110
|
+
"""
|
|
111
|
+
p = self.params
|
|
112
|
+
alpha, beta, delta, mu = p["alpha"], p["beta"], p["delta"], p["mu"]
|
|
113
|
+
gamma_0 = np.sqrt(alpha**2 - beta**2)
|
|
114
|
+
|
|
115
|
+
def phi_raw(u: np.ndarray | complex) -> np.ndarray | complex:
|
|
116
|
+
gamma_u = np.sqrt(alpha**2 - (beta + 1j * u) ** 2)
|
|
117
|
+
term1 = np.exp(1j * u * mu * t)
|
|
118
|
+
term2 = (gamma_0 / gamma_u) ** t * (
|
|
119
|
+
kv(1, delta * t * gamma_u) / kv(1, delta * t * gamma_0)
|
|
120
|
+
)
|
|
121
|
+
return term1 * term2
|
|
122
|
+
|
|
123
|
+
return phi_raw
|
|
124
|
+
|
|
125
|
+
def sample_terminal_log_return(
|
|
126
|
+
self,
|
|
127
|
+
T: float,
|
|
128
|
+
size: int,
|
|
129
|
+
rng: np.random.Generator,
|
|
130
|
+
) -> np.ndarray:
|
|
131
|
+
"""
|
|
132
|
+
Generates exact samples of the terminal log-return for the Hyperbolic process.
|
|
133
|
+
|
|
134
|
+
This uses the `scipy.stats.genhyperbolic` distribution, which is the
|
|
135
|
+
exact distribution of the process at a given time horizon.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
T : float
|
|
140
|
+
The time to maturity, in years.
|
|
141
|
+
size : int
|
|
142
|
+
The number of samples to generate.
|
|
143
|
+
rng : np.random.Generator
|
|
144
|
+
A random number generator instance for reproducibility.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
np.ndarray
|
|
149
|
+
An array of simulated terminal log-returns.
|
|
150
|
+
"""
|
|
151
|
+
p = self.params
|
|
152
|
+
return genhyperbolic.rvs(
|
|
153
|
+
p=1.0,
|
|
154
|
+
a=p["alpha"],
|
|
155
|
+
b=p["beta"],
|
|
156
|
+
loc=p["mu"] * T,
|
|
157
|
+
scale=p["delta"] * T,
|
|
158
|
+
size=size,
|
|
159
|
+
random_state=rng,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Abstract Method Implementations
|
|
163
|
+
def _sde_impl(self, **kwargs: Any) -> Any:
|
|
164
|
+
raise NotImplementedError
|
|
165
|
+
|
|
166
|
+
def _pde_impl(self, **kwargs: Any) -> Any:
|
|
167
|
+
raise NotImplementedError
|
|
168
|
+
|
|
169
|
+
def _closed_form_impl(self, **kwargs: Any) -> Any:
|
|
170
|
+
raise NotImplementedError
|
quantfin/models/kou.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from quantfin.models.base import CF, BaseModel, ParamValidator
|
|
8
|
+
|
|
9
|
+
__doc__ = """
|
|
10
|
+
Defines the Kou double-exponential jump-diffusion model.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class KouModel(BaseModel):
|
|
15
|
+
"""Kou double-exponential jump-diffusion model."""
|
|
16
|
+
|
|
17
|
+
name: str = "Kou Double-Exponential Jump"
|
|
18
|
+
supports_cf: bool = True
|
|
19
|
+
supports_sde: bool = True
|
|
20
|
+
has_jumps: bool = True
|
|
21
|
+
|
|
22
|
+
default_params = {
|
|
23
|
+
"sigma": 0.15,
|
|
24
|
+
"lambda": 1.0,
|
|
25
|
+
"p_up": 0.6,
|
|
26
|
+
"eta1": 10.0,
|
|
27
|
+
"eta2": 5.0,
|
|
28
|
+
}
|
|
29
|
+
param_defs = {
|
|
30
|
+
"sigma": {
|
|
31
|
+
"label": "Volatility",
|
|
32
|
+
"default": 0.15,
|
|
33
|
+
"min": 0.01,
|
|
34
|
+
"max": 1.0,
|
|
35
|
+
"step": 0.01,
|
|
36
|
+
},
|
|
37
|
+
"lambda": {
|
|
38
|
+
"label": "Jump Intensity",
|
|
39
|
+
"default": 1.0,
|
|
40
|
+
"min": 0.0,
|
|
41
|
+
"max": 10.0,
|
|
42
|
+
"step": 0.1,
|
|
43
|
+
},
|
|
44
|
+
"p_up": {
|
|
45
|
+
"label": "Up Prob",
|
|
46
|
+
"default": 0.6,
|
|
47
|
+
"min": 0.0,
|
|
48
|
+
"max": 1.0,
|
|
49
|
+
"step": 0.05,
|
|
50
|
+
},
|
|
51
|
+
"eta1": {
|
|
52
|
+
"label": "Up Size",
|
|
53
|
+
"default": 10.0,
|
|
54
|
+
"min": 0.1,
|
|
55
|
+
"max": 50.0,
|
|
56
|
+
"step": 0.5,
|
|
57
|
+
},
|
|
58
|
+
"eta2": {
|
|
59
|
+
"label": "Down Size",
|
|
60
|
+
"default": 5.0,
|
|
61
|
+
"min": 0.1,
|
|
62
|
+
"max": 50.0,
|
|
63
|
+
"step": 0.5,
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def __init__(self, params: dict[str, float] | None = None):
|
|
68
|
+
"""
|
|
69
|
+
Initializes the Kou Double-Exponential Jump model.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
params : dict[str, float] | None, optional
|
|
74
|
+
A dictionary of model parameters. If None, `default_params` are used.
|
|
75
|
+
"""
|
|
76
|
+
super().__init__(params or self.default_params)
|
|
77
|
+
|
|
78
|
+
def _validate_params(self) -> None:
|
|
79
|
+
"""Validate the models parameter."""
|
|
80
|
+
p = self.params
|
|
81
|
+
req = ["sigma", "lambda", "p_up", "eta1", "eta2"]
|
|
82
|
+
ParamValidator.require(p, req, model=self.name)
|
|
83
|
+
ParamValidator.positive(p, ["sigma", "lambda", "eta1", "eta2"], model=self.name)
|
|
84
|
+
ParamValidator.bounded(p, "p_up", 0.0, 1.0, model=self.name)
|
|
85
|
+
|
|
86
|
+
def __eq__(self, other: object) -> bool:
|
|
87
|
+
if not isinstance(other, KouModel):
|
|
88
|
+
return NotImplemented
|
|
89
|
+
return self.params == other.params
|
|
90
|
+
|
|
91
|
+
def __hash__(self) -> int:
|
|
92
|
+
return hash((self.__class__, tuple(sorted(self.params.items()))))
|
|
93
|
+
|
|
94
|
+
def _cf_impl(self, *, t: float, spot: float, r: float, q: float, **_: Any) -> CF:
|
|
95
|
+
"""
|
|
96
|
+
Kou characteristic function for the log-spot price log(S_t).
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
t : float
|
|
101
|
+
The time to maturity of the option, in years.
|
|
102
|
+
spot : float
|
|
103
|
+
The current price of the underlying asset.
|
|
104
|
+
r : float
|
|
105
|
+
The continuously compounded risk-free rate.
|
|
106
|
+
q : float
|
|
107
|
+
The continuously compounded dividend yield.
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
CF
|
|
112
|
+
The characteristic function.
|
|
113
|
+
"""
|
|
114
|
+
p = self.params
|
|
115
|
+
sigma, lambda_, p_up, eta1, eta2 = (
|
|
116
|
+
p["sigma"],
|
|
117
|
+
p["lambda"],
|
|
118
|
+
p["p_up"],
|
|
119
|
+
p["eta1"],
|
|
120
|
+
p["eta2"],
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
compensator = lambda_ * (p_up / (eta1 - 1) - (1 - p_up) / (eta2 + 1))
|
|
124
|
+
drift = r - q - 0.5 * sigma**2 - compensator
|
|
125
|
+
|
|
126
|
+
def phi(u: np.ndarray | complex) -> np.ndarray | complex:
|
|
127
|
+
# BSM component
|
|
128
|
+
bsm_part = 1j * u * (np.log(spot) + drift * t) - 0.5 * u**2 * sigma**2 * t
|
|
129
|
+
# Jump component formulation
|
|
130
|
+
jump_part = (
|
|
131
|
+
lambda_
|
|
132
|
+
* t
|
|
133
|
+
* (p_up / (eta1 - 1j * u) + (1 - p_up) / (eta2 + 1j * u) - 1)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return np.exp(bsm_part + jump_part)
|
|
137
|
+
|
|
138
|
+
return phi
|
|
139
|
+
|
|
140
|
+
# Abstract Method Implementations
|
|
141
|
+
def _sde_impl(self, **kwargs: Any) -> Any:
|
|
142
|
+
raise NotImplementedError("Kou uses a specialized kernel.")
|
|
143
|
+
|
|
144
|
+
def _pde_impl(self, **kwargs: Any) -> Any:
|
|
145
|
+
raise NotImplementedError
|
|
146
|
+
|
|
147
|
+
def _closed_form_impl(self, **kwargs: Any) -> Any:
|
|
148
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from scipy.special import factorial
|
|
8
|
+
|
|
9
|
+
from quantfin.models.base import CF, BaseModel, ParamValidator
|
|
10
|
+
from quantfin.models.bsm import BSMModel
|
|
11
|
+
|
|
12
|
+
__doc__ = """
|
|
13
|
+
Defines the Merton jump-diffusion model.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MertonJumpModel(BaseModel):
|
|
18
|
+
"""
|
|
19
|
+
Merton Jump-Diffusion model, implemented as a Poisson-weighted sum of BSM prices.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
name: str = "Merton Jump-Diffusion"
|
|
23
|
+
supports_cf: bool = True
|
|
24
|
+
has_closed_form: bool = True
|
|
25
|
+
supports_sde: bool = True
|
|
26
|
+
has_jumps: bool = True
|
|
27
|
+
|
|
28
|
+
default_params = {
|
|
29
|
+
"sigma": 0.2,
|
|
30
|
+
"lambda": 0.5,
|
|
31
|
+
"mu_j": -0.1,
|
|
32
|
+
"sigma_j": 0.15,
|
|
33
|
+
"max_sum_terms": 100,
|
|
34
|
+
}
|
|
35
|
+
param_defs = {
|
|
36
|
+
"sigma": {
|
|
37
|
+
"label": "Volatility",
|
|
38
|
+
"default": 0.2,
|
|
39
|
+
"min": 0.01,
|
|
40
|
+
"max": 1.0,
|
|
41
|
+
"step": 0.01,
|
|
42
|
+
},
|
|
43
|
+
"lambda": {
|
|
44
|
+
"label": "Jump Intensity",
|
|
45
|
+
"default": 0.5,
|
|
46
|
+
"min": 0.0,
|
|
47
|
+
"max": 5.0,
|
|
48
|
+
"step": 0.1,
|
|
49
|
+
},
|
|
50
|
+
"mu_j": {
|
|
51
|
+
"label": "Mean Jump",
|
|
52
|
+
"default": -0.1,
|
|
53
|
+
"min": -1.0,
|
|
54
|
+
"max": 1.0,
|
|
55
|
+
"step": 0.05,
|
|
56
|
+
},
|
|
57
|
+
"sigma_j": {
|
|
58
|
+
"label": "Jump Vol",
|
|
59
|
+
"default": 0.15,
|
|
60
|
+
"min": 0.01,
|
|
61
|
+
"max": 1.0,
|
|
62
|
+
"step": 0.01,
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
def __init__(self, params: dict[str, float]):
|
|
67
|
+
"""
|
|
68
|
+
Initializes the Merton Jump-Diffusion model.
|
|
69
|
+
Uses BSMModel instance for the core diffusion
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
params : dict[str, float]
|
|
74
|
+
A dictionary of model parameters.
|
|
75
|
+
"""
|
|
76
|
+
super().__init__(params)
|
|
77
|
+
self.bsm_solver = BSMModel(params={"sigma": self.params["sigma"]})
|
|
78
|
+
|
|
79
|
+
def _validate_params(self) -> None:
|
|
80
|
+
"""Validates parameters for diffusion and jump components."""
|
|
81
|
+
p = self.params
|
|
82
|
+
req = ["lambda", "mu_j", "sigma_j", "sigma", "max_sum_terms"]
|
|
83
|
+
ParamValidator.require(p, req, model=self.name)
|
|
84
|
+
ParamValidator.positive(
|
|
85
|
+
p, ["lambda", "sigma_j", "sigma", "max_sum_terms"], model=self.name
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def _closed_form_impl(
|
|
89
|
+
self,
|
|
90
|
+
*,
|
|
91
|
+
spot: float,
|
|
92
|
+
strike: float,
|
|
93
|
+
r: float,
|
|
94
|
+
q: float,
|
|
95
|
+
t: float,
|
|
96
|
+
call: bool = True,
|
|
97
|
+
) -> float:
|
|
98
|
+
"""
|
|
99
|
+
Merton's closed-form solution as a sum of BSM prices.
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
spot : float
|
|
104
|
+
The current price of the underlying asset.
|
|
105
|
+
strike : float
|
|
106
|
+
The strike price of the option.
|
|
107
|
+
r : float
|
|
108
|
+
The continuously compounded risk-free rate.
|
|
109
|
+
q : float
|
|
110
|
+
The continuously compounded dividend yield.
|
|
111
|
+
t : float
|
|
112
|
+
The time to maturity of the option, in years.
|
|
113
|
+
call : bool, optional
|
|
114
|
+
True for a call option, False for a put. Defaults to True.
|
|
115
|
+
|
|
116
|
+
Returns
|
|
117
|
+
-------
|
|
118
|
+
float
|
|
119
|
+
The price of the European option.
|
|
120
|
+
"""
|
|
121
|
+
lambda_ = self.params["lambda"]
|
|
122
|
+
mu_j = self.params["mu_j"]
|
|
123
|
+
sigma_j = self.params["sigma_j"]
|
|
124
|
+
max_sum_terms = int(self.params["max_sum_terms"])
|
|
125
|
+
|
|
126
|
+
k = math.exp(mu_j + 0.5 * sigma_j**2) - 1
|
|
127
|
+
lambda_prime = lambda_ * (1 + k)
|
|
128
|
+
y_mul = lambda_prime * t
|
|
129
|
+
|
|
130
|
+
interval = np.arange(max_sum_terms)
|
|
131
|
+
weights = np.exp(-y_mul) * (y_mul**interval) / factorial(interval)
|
|
132
|
+
|
|
133
|
+
r_n = r - lambda_ * k + (interval * (mu_j + 0.5 * sigma_j**2)) / t
|
|
134
|
+
sigma_n_sq = self.params["sigma"] ** 2 + (interval * sigma_j**2) / t
|
|
135
|
+
sigma_n = np.sqrt(np.maximum(sigma_n_sq, 1e-12))
|
|
136
|
+
|
|
137
|
+
total_price = 0.0
|
|
138
|
+
for i in range(max_sum_terms):
|
|
139
|
+
# Terminate sum early if weights become negligible
|
|
140
|
+
if weights[i] < 1e-12 and i > lambda_prime * t:
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
temp_bsm_solver = self.bsm_solver.with_params(sigma=sigma_n[i])
|
|
144
|
+
|
|
145
|
+
price_term = temp_bsm_solver.price_closed_form(
|
|
146
|
+
spot=spot, strike=strike, r=r_n[i], q=q, t=t, call=call
|
|
147
|
+
)
|
|
148
|
+
total_price += weights[i] * price_term
|
|
149
|
+
|
|
150
|
+
return total_price
|
|
151
|
+
|
|
152
|
+
def _cf_impl(
|
|
153
|
+
self,
|
|
154
|
+
*,
|
|
155
|
+
t: float,
|
|
156
|
+
spot: float,
|
|
157
|
+
r: float,
|
|
158
|
+
q: float,
|
|
159
|
+
**_: Any,
|
|
160
|
+
) -> CF:
|
|
161
|
+
"""
|
|
162
|
+
Merton characteristic function for the log-spot price log(S_t).
|
|
163
|
+
|
|
164
|
+
Parameters
|
|
165
|
+
----------
|
|
166
|
+
t : float
|
|
167
|
+
The time to maturity of the option, in years.
|
|
168
|
+
spot : float
|
|
169
|
+
The current price of the underlying asset.
|
|
170
|
+
r : float
|
|
171
|
+
The continuously compounded risk-free rate.
|
|
172
|
+
q : float
|
|
173
|
+
The continuously compounded dividend yield.
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
CF
|
|
178
|
+
The characteristic function.
|
|
179
|
+
"""
|
|
180
|
+
p = self.params
|
|
181
|
+
sigma, lambda_, mu_j, sigma_j = p["sigma"], p["lambda"], p["mu_j"], p["sigma_j"]
|
|
182
|
+
drift = r - q - 0.5 * sigma**2
|
|
183
|
+
compensator = lambda_ * (np.exp(mu_j + 0.5 * sigma_j**2) - 1)
|
|
184
|
+
|
|
185
|
+
def phi(u: np.ndarray | complex) -> np.ndarray | complex:
|
|
186
|
+
bsm_part = (
|
|
187
|
+
1j * u * (np.log(spot) + (drift - compensator) * t)
|
|
188
|
+
- 0.5 * u**2 * sigma**2 * t
|
|
189
|
+
)
|
|
190
|
+
jump_part = (
|
|
191
|
+
lambda_ * t * (np.exp(1j * u * mu_j - 0.5 * u**2 * sigma_j**2) - 1)
|
|
192
|
+
)
|
|
193
|
+
return np.exp(bsm_part + jump_part)
|
|
194
|
+
|
|
195
|
+
return phi
|
|
196
|
+
|
|
197
|
+
def _sde_impl(self):
|
|
198
|
+
p = self.params
|
|
199
|
+
sigma, mu_j, sigma_j, lambda_ = p["sigma"], p["mu_j"], p["sigma_j"], p["lambda"]
|
|
200
|
+
compensator = lambda_ * (np.exp(mu_j + 0.5 * sigma_j**2) - 1)
|
|
201
|
+
|
|
202
|
+
def stepper(
|
|
203
|
+
log_s_t: np.ndarray,
|
|
204
|
+
r: float,
|
|
205
|
+
q: float,
|
|
206
|
+
dt: float,
|
|
207
|
+
dw_t: np.ndarray,
|
|
208
|
+
jump_counts: np.ndarray,
|
|
209
|
+
rng: np.random.Generator,
|
|
210
|
+
) -> np.ndarray:
|
|
211
|
+
drift_term = (r - q - 0.5 * sigma**2 - compensator) * dt
|
|
212
|
+
next_log_s = log_s_t + drift_term + sigma * dw_t
|
|
213
|
+
paths_with_jumps = np.where(jump_counts > 0)[0]
|
|
214
|
+
for path_idx in paths_with_jumps:
|
|
215
|
+
num_jumps_on_path = jump_counts[path_idx]
|
|
216
|
+
total_jump_size = np.sum(
|
|
217
|
+
rng.normal(loc=mu_j, scale=sigma_j, size=num_jumps_on_path)
|
|
218
|
+
)
|
|
219
|
+
next_log_s[path_idx] += total_jump_size
|
|
220
|
+
return next_log_s
|
|
221
|
+
|
|
222
|
+
return stepper
|
|
223
|
+
|
|
224
|
+
def _pde_impl(self) -> Any:
|
|
225
|
+
raise NotImplementedError(f"{self.name} PDE not implemented")
|