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
quantfin/models/cgmy.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
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 gamma as gamma_func
|
|
8
|
+
|
|
9
|
+
from quantfin.models.base import CF, BaseModel, ParamValidator
|
|
10
|
+
|
|
11
|
+
__doc__ = """
|
|
12
|
+
Defines the CGMY (Carr, Geman, Madan, Yor, 2002) pure-jump Lévy model.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CGMYModel(BaseModel):
|
|
17
|
+
"""
|
|
18
|
+
CGMY (Carr, Geman, Madan, Yor, 2002) pure-jump Lévy model.
|
|
19
|
+
|
|
20
|
+
This is a flexible four-parameter model that can capture skewness and kurtosis.
|
|
21
|
+
It has a known characteristic function for all valid parameters. Monte Carlo
|
|
22
|
+
simulation is supported for the special case Y=1 (Variance Gamma process).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
name: str = "CGMY"
|
|
26
|
+
supports_cf: bool = True
|
|
27
|
+
is_pure_levy: bool = True
|
|
28
|
+
|
|
29
|
+
default_params = {"C": 0.02, "G": 5.0, "M": 5.0, "Y": 1.2}
|
|
30
|
+
|
|
31
|
+
def __init__(self, params: dict[str, float] | None = None):
|
|
32
|
+
"""
|
|
33
|
+
Initializes the CGMY model.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
params : dict[str, float] | None, optional
|
|
38
|
+
A dictionary of model parameters. If None, `default_params` are used.
|
|
39
|
+
Defaults to None.
|
|
40
|
+
"""
|
|
41
|
+
super().__init__(params or self.default_params)
|
|
42
|
+
|
|
43
|
+
def _validate_params(self) -> None:
|
|
44
|
+
"""Validates the C, G, M, and Y parameters."""
|
|
45
|
+
p = self.params
|
|
46
|
+
req = ["C", "G", "M", "Y"]
|
|
47
|
+
ParamValidator.require(p, req, model=self.name)
|
|
48
|
+
ParamValidator.positive(p, ["C", "G", "M"], model=self.name)
|
|
49
|
+
if not (p["Y"] < 2):
|
|
50
|
+
raise ValueError(
|
|
51
|
+
"CGMY parameter Y must be less than 2 for finite variance."
|
|
52
|
+
)
|
|
53
|
+
if p["Y"] <= 0 and p["C"] != 0:
|
|
54
|
+
print("Warning: CGMY with Y<=0 and C!=0 can have infinite moments.")
|
|
55
|
+
|
|
56
|
+
def __eq__(self, other: object) -> bool:
|
|
57
|
+
if not isinstance(other, CGMYModel):
|
|
58
|
+
return NotImplemented
|
|
59
|
+
return self.params == other.params
|
|
60
|
+
|
|
61
|
+
def __hash__(self) -> int:
|
|
62
|
+
return hash((self.__class__, tuple(sorted(self.params.items()))))
|
|
63
|
+
|
|
64
|
+
def _cf_impl(
|
|
65
|
+
self,
|
|
66
|
+
*,
|
|
67
|
+
t: float,
|
|
68
|
+
spot: float,
|
|
69
|
+
r: float,
|
|
70
|
+
q: float,
|
|
71
|
+
**_: Any,
|
|
72
|
+
) -> CF:
|
|
73
|
+
"""
|
|
74
|
+
Risk-neutral characteristic function for the log-spot price log(S_t).
|
|
75
|
+
|
|
76
|
+
This is the risk-neutral characteristic function, which includes the
|
|
77
|
+
drift adjustment.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
t : float
|
|
82
|
+
The time to maturity of the option, in years.
|
|
83
|
+
spot : float
|
|
84
|
+
The current price of the underlying asset.
|
|
85
|
+
r : float
|
|
86
|
+
The continuously compounded risk-free rate.
|
|
87
|
+
q : float
|
|
88
|
+
The continuously compounded dividend yield.
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
CF
|
|
93
|
+
The characteristic function.
|
|
94
|
+
"""
|
|
95
|
+
compensator = np.log(self.raw_cf(t=1.0)(-1j))
|
|
96
|
+
drift = r - q - np.real(compensator)
|
|
97
|
+
|
|
98
|
+
def phi(u: np.ndarray | complex) -> np.ndarray | complex:
|
|
99
|
+
return self.raw_cf(t=t)(u) * np.exp(1j * u * (np.log(spot) + drift * t))
|
|
100
|
+
|
|
101
|
+
return phi
|
|
102
|
+
|
|
103
|
+
def raw_cf(self, *, t: float) -> Callable:
|
|
104
|
+
"""
|
|
105
|
+
Returns the CF of the raw CGMY process, without drift or spot.
|
|
106
|
+
|
|
107
|
+
This function represents the characteristic function of the process
|
|
108
|
+
before the risk-neutral drift adjustment is applied.
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
t : float
|
|
113
|
+
The time horizon.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
Callable
|
|
118
|
+
The raw characteristic function.
|
|
119
|
+
"""
|
|
120
|
+
p = self.params
|
|
121
|
+
C, G, M, Y = p["C"], p["G"], p["M"], p["Y"]
|
|
122
|
+
|
|
123
|
+
def phi_raw(u: np.ndarray | complex) -> np.ndarray | complex:
|
|
124
|
+
term = (
|
|
125
|
+
C
|
|
126
|
+
* t
|
|
127
|
+
* gamma_func(-Y)
|
|
128
|
+
* ((M - 1j * u) ** Y - M**Y + (G + 1j * u) ** Y - G**Y)
|
|
129
|
+
)
|
|
130
|
+
return np.exp(term)
|
|
131
|
+
|
|
132
|
+
return phi_raw
|
|
133
|
+
|
|
134
|
+
def sample_terminal_log_return(
|
|
135
|
+
self,
|
|
136
|
+
T: float,
|
|
137
|
+
size: int,
|
|
138
|
+
rng: np.random.Generator,
|
|
139
|
+
) -> np.ndarray:
|
|
140
|
+
"""
|
|
141
|
+
Generates exact samples of the terminal log-return for the CGMY process.
|
|
142
|
+
|
|
143
|
+
NOTE: This is only implemented for the special case Y=1, where the
|
|
144
|
+
process is a difference of two time-changed Brownian motions.
|
|
145
|
+
|
|
146
|
+
Parameters
|
|
147
|
+
----------
|
|
148
|
+
T : float
|
|
149
|
+
The time to maturity, in years.
|
|
150
|
+
size : int
|
|
151
|
+
The number of samples to generate.
|
|
152
|
+
rng : np.random.Generator
|
|
153
|
+
A random number generator instance for reproducibility.
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
np.ndarray
|
|
158
|
+
An array of simulated terminal log-returns.
|
|
159
|
+
|
|
160
|
+
Raises
|
|
161
|
+
------
|
|
162
|
+
NotImplementedError
|
|
163
|
+
If the model parameter Y is not equal to 1.
|
|
164
|
+
"""
|
|
165
|
+
p = self.params
|
|
166
|
+
C, G, M, Y = p["C"], p["G"], p["M"], p["Y"]
|
|
167
|
+
|
|
168
|
+
if not np.isclose(Y, 1.0):
|
|
169
|
+
raise NotImplementedError(
|
|
170
|
+
"Monte Carlo sampling for CGMY is only implemented for Y=1."
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# For Y=1, CGMY is a scaled difference of two Gamma processes.
|
|
174
|
+
# This is equivalent to a Variance Gamma process.
|
|
175
|
+
# Can sample this by time-changing a Brownian motion.
|
|
176
|
+
g_up = rng.gamma(shape=C * T, scale=1 / M, size=size)
|
|
177
|
+
g_down = rng.gamma(shape=C * T, scale=1 / G, size=size)
|
|
178
|
+
|
|
179
|
+
return g_up - g_down
|
|
180
|
+
|
|
181
|
+
# Abstract Method Implementations
|
|
182
|
+
def _sde_impl(self, **kwargs: Any) -> Any:
|
|
183
|
+
raise NotImplementedError("CGMY is a pure Lévy process, use terminal sampling.")
|
|
184
|
+
|
|
185
|
+
def _pde_impl(self, **kwargs: Any) -> Any:
|
|
186
|
+
raise NotImplementedError
|
|
187
|
+
|
|
188
|
+
def _closed_form_impl(self, **kwargs: Any) -> Any:
|
|
189
|
+
raise NotImplementedError
|
quantfin/models/cir.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from quantfin.models.base import BaseModel, ParamValidator
|
|
7
|
+
|
|
8
|
+
__doc__ = """
|
|
9
|
+
Defines the Cox-Ingersoll-Ross (1985) mean-reverting interest rate model.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CIRModel(BaseModel):
|
|
14
|
+
"""
|
|
15
|
+
Cox-Ingersoll-Ross (1985) mean-reverting short rate model.
|
|
16
|
+
dr_t = kappa * (theta - r_t) * dt + sigma * sqrt(r_t) * dW_t
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
name: str = "Cox-Ingersoll-Ross"
|
|
20
|
+
has_closed_form: bool = True
|
|
21
|
+
default_params = {"kappa": 0.86, "theta": 0.09, "sigma": 0.02}
|
|
22
|
+
|
|
23
|
+
def __init__(self, params: dict[str, float] | None = None):
|
|
24
|
+
"""
|
|
25
|
+
Initializes the CIR model.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
params : dict[str, float] | None, optional
|
|
30
|
+
A dictionary of model parameters. If None, `default_params` are used.
|
|
31
|
+
"""
|
|
32
|
+
super().__init__(params or self.default_params)
|
|
33
|
+
|
|
34
|
+
def _validate_params(self) -> None:
|
|
35
|
+
p = self.params
|
|
36
|
+
req = ["kappa", "theta", "sigma"]
|
|
37
|
+
ParamValidator.require(p, req, model=self.name)
|
|
38
|
+
ParamValidator.positive(p, ["kappa", "theta", "sigma"], model=self.name)
|
|
39
|
+
if 2 * p["kappa"] * p["theta"] < p["sigma"] ** 2:
|
|
40
|
+
print("Warning: CIR parameters do not satisfy the Feller condition.")
|
|
41
|
+
|
|
42
|
+
def _closed_form_impl(
|
|
43
|
+
self,
|
|
44
|
+
*,
|
|
45
|
+
spot: float,
|
|
46
|
+
t: float,
|
|
47
|
+
**_: Any,
|
|
48
|
+
) -> float:
|
|
49
|
+
"""
|
|
50
|
+
Calculates the price of a Zero-Coupon Bond.
|
|
51
|
+
|
|
52
|
+
Note: Re-interprets 'spot' as the initial short rate r0 and 't' as
|
|
53
|
+
the bond's maturity T. Ignores other option-specific parameters.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
spot : float
|
|
58
|
+
The initial short rate, r0.
|
|
59
|
+
t : float
|
|
60
|
+
The maturity of the bond, in years.
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
float
|
|
65
|
+
The price of the zero-coupon bond.
|
|
66
|
+
"""
|
|
67
|
+
r0, T = spot, t
|
|
68
|
+
p = self.params
|
|
69
|
+
kappa, theta, sigma = p["kappa"], p["theta"], p["sigma"]
|
|
70
|
+
|
|
71
|
+
gamma = math.sqrt(kappa**2 + 2 * sigma**2)
|
|
72
|
+
exp_gamma_T = math.exp(gamma * T)
|
|
73
|
+
|
|
74
|
+
den = (gamma + kappa) * (exp_gamma_T - 1) + 2 * gamma
|
|
75
|
+
B = 2 * (exp_gamma_T - 1) / den
|
|
76
|
+
A_log_base = (2 * gamma * math.exp((kappa + gamma) * T / 2)) / den
|
|
77
|
+
A_log_power = (2 * kappa * theta) / sigma**2
|
|
78
|
+
|
|
79
|
+
price = (A_log_base**A_log_power) * math.exp(-B * r0)
|
|
80
|
+
return price
|
|
81
|
+
|
|
82
|
+
# Abstract Method Implementations
|
|
83
|
+
def _sde_impl(self, **kwargs: Any) -> Any:
|
|
84
|
+
raise NotImplementedError
|
|
85
|
+
|
|
86
|
+
def _cf_impl(self, **kwargs: Any) -> Any:
|
|
87
|
+
raise NotImplementedError
|
|
88
|
+
|
|
89
|
+
def _pde_impl(self, **kwargs: Any) -> Any:
|
|
90
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from quantfin.models.base import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DupireLocalVolModel(BaseModel):
|
|
9
|
+
"""Dupire Local Volatility model."""
|
|
10
|
+
|
|
11
|
+
name: str = "Dupire Local Volatility"
|
|
12
|
+
supports_sde: bool = True
|
|
13
|
+
is_local_vol: bool = True
|
|
14
|
+
|
|
15
|
+
default_params = {"vol_surface": lambda T, K: 0.2}
|
|
16
|
+
|
|
17
|
+
def __init__(self, params: dict[str, Any] | None = None):
|
|
18
|
+
"""
|
|
19
|
+
Initializes the Dupire Local Volatility model.
|
|
20
|
+
|
|
21
|
+
The key parameter for this model is 'vol_surface', a callable function
|
|
22
|
+
that takes maturity (T) and strike (K) and returns the local volatility.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
params : dict[str, Any] | None, optional
|
|
27
|
+
A dictionary of model parameters. If None, a default constant
|
|
28
|
+
volatility surface is used. Defaults to None.
|
|
29
|
+
"""
|
|
30
|
+
super().__init__(params or self.default_params)
|
|
31
|
+
|
|
32
|
+
def _validate_params(self) -> None:
|
|
33
|
+
if "vol_surface" not in self.params or not callable(self.params["vol_surface"]):
|
|
34
|
+
raise ValueError(
|
|
35
|
+
"Dupire model requires a callable 'vol_surface' in its parameters."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def __eq__(self, other: object) -> bool:
|
|
39
|
+
if not isinstance(other, DupireLocalVolModel):
|
|
40
|
+
return NotImplemented
|
|
41
|
+
return self.params == other.params
|
|
42
|
+
|
|
43
|
+
def __hash__(self) -> int:
|
|
44
|
+
return hash((self.__class__, tuple(sorted(self.params.items()))))
|
|
45
|
+
|
|
46
|
+
def __repr__(self) -> str:
|
|
47
|
+
"""Custom representation to handle the vol_surface function."""
|
|
48
|
+
# Get the name of the function if possible, otherwise show its type
|
|
49
|
+
vol_surface_repr = getattr(
|
|
50
|
+
self.params["vol_surface"],
|
|
51
|
+
"__name__",
|
|
52
|
+
str(type(self.params["vol_surface"])),
|
|
53
|
+
)
|
|
54
|
+
return f"{self.__class__.__name__}(vol_surface={vol_surface_repr})"
|
|
55
|
+
|
|
56
|
+
# Abstract Method Implementations
|
|
57
|
+
def _sde_impl(self, **kwargs: Any) -> Any:
|
|
58
|
+
raise NotImplementedError("Dupire uses a specialized kernel.")
|
|
59
|
+
|
|
60
|
+
def _cf_impl(self, **kwargs: Any) -> Any:
|
|
61
|
+
raise NotImplementedError
|
|
62
|
+
|
|
63
|
+
def _pde_impl(self, **kwargs: Any) -> Any:
|
|
64
|
+
raise NotImplementedError
|
|
65
|
+
|
|
66
|
+
def _closed_form_impl(self, **kwargs: Any) -> Any:
|
|
67
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from quantfin.models.base import CF, BaseModel, ParamValidator
|
|
9
|
+
|
|
10
|
+
__doc__ = """
|
|
11
|
+
Defines the Heston stochastic volatility model.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HestonModel(BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
Heston stochastic volatility model.
|
|
18
|
+
|
|
19
|
+
This model describes the evolution of an asset's price where the volatility
|
|
20
|
+
itself is a random process, following a Cox-Ingersoll-Ross (CIR) process.
|
|
21
|
+
It is widely used as it can capture volatility smiles and skews.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
name: str = "Heston"
|
|
25
|
+
supports_cf: bool = True
|
|
26
|
+
supports_sde: bool = True
|
|
27
|
+
has_variance_process: bool = True
|
|
28
|
+
cf_kwargs = ("v0",) # v0 is required for pricing, passed from kwargs
|
|
29
|
+
|
|
30
|
+
default_params = {
|
|
31
|
+
"v0": 0.04,
|
|
32
|
+
"kappa": 2.0,
|
|
33
|
+
"theta": 0.04,
|
|
34
|
+
"rho": -0.7,
|
|
35
|
+
"vol_of_vol": 0.5,
|
|
36
|
+
}
|
|
37
|
+
param_defs = {
|
|
38
|
+
"v0": {
|
|
39
|
+
"label": "Initial Variance (v0)",
|
|
40
|
+
"default": 0.04,
|
|
41
|
+
"min": 0.001,
|
|
42
|
+
"max": 0.5,
|
|
43
|
+
"step": 0.01,
|
|
44
|
+
},
|
|
45
|
+
"kappa": {
|
|
46
|
+
"label": "Mean Reversion (κ)",
|
|
47
|
+
"default": 2.0,
|
|
48
|
+
"min": 0.1,
|
|
49
|
+
"max": 10.0,
|
|
50
|
+
"step": 0.1,
|
|
51
|
+
},
|
|
52
|
+
"theta": {
|
|
53
|
+
"label": "Long-Term Var (θ)",
|
|
54
|
+
"default": 0.04,
|
|
55
|
+
"min": 0.01,
|
|
56
|
+
"max": 0.5,
|
|
57
|
+
"step": 0.01,
|
|
58
|
+
},
|
|
59
|
+
"rho": {
|
|
60
|
+
"label": "Correlation (ρ)",
|
|
61
|
+
"default": -0.7,
|
|
62
|
+
"min": -0.99,
|
|
63
|
+
"max": 0.99,
|
|
64
|
+
"step": 0.05,
|
|
65
|
+
},
|
|
66
|
+
"vol_of_vol": {
|
|
67
|
+
"label": "Vol of Vol (ξ)",
|
|
68
|
+
"default": 0.5,
|
|
69
|
+
"min": 0.1,
|
|
70
|
+
"max": 1.5,
|
|
71
|
+
"step": 0.05,
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
def __init__(self, params: dict[str, float] | None = None):
|
|
76
|
+
"""
|
|
77
|
+
Initializes the Heston model.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
params : dict[str, float] | None, optional
|
|
82
|
+
A dictionary of model parameters. If None, `default_params` are used.
|
|
83
|
+
"""
|
|
84
|
+
super().__init__(params or self.default_params)
|
|
85
|
+
|
|
86
|
+
def _validate_params(self) -> None:
|
|
87
|
+
"""Validates parameters for mean-reversion, correlation, etc."""
|
|
88
|
+
p = self.params
|
|
89
|
+
req = ["kappa", "theta", "rho", "vol_of_vol"]
|
|
90
|
+
ParamValidator.require(p, req, model=self.name)
|
|
91
|
+
ParamValidator.positive(p, ["kappa", "theta", "vol_of_vol"], model=self.name)
|
|
92
|
+
ParamValidator.bounded(p, "rho", -1.0, 1.0, model=self.name)
|
|
93
|
+
if 2 * p["kappa"] * p["theta"] < p["vol_of_vol"] ** 2:
|
|
94
|
+
print(
|
|
95
|
+
f"Warning: Params for {self.name} do not satisfy the Feller condition."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def __eq__(self, other: object) -> bool:
|
|
99
|
+
if not isinstance(other, HestonModel):
|
|
100
|
+
return NotImplemented
|
|
101
|
+
return self.params == other.params
|
|
102
|
+
|
|
103
|
+
def __hash__(self) -> int:
|
|
104
|
+
return hash((self.__class__, tuple(sorted(self.params.items()))))
|
|
105
|
+
|
|
106
|
+
def _cf_impl(
|
|
107
|
+
self,
|
|
108
|
+
*,
|
|
109
|
+
t: float,
|
|
110
|
+
spot: float,
|
|
111
|
+
r: float,
|
|
112
|
+
q: float,
|
|
113
|
+
v0: float,
|
|
114
|
+
**_: Any,
|
|
115
|
+
) -> CF:
|
|
116
|
+
"""
|
|
117
|
+
Heston characteristic function for the log-spot price log(S_t).
|
|
118
|
+
|
|
119
|
+
Parameters
|
|
120
|
+
----------
|
|
121
|
+
t : float
|
|
122
|
+
The time to maturity of the option, in years.
|
|
123
|
+
spot : float
|
|
124
|
+
The current price of the underlying asset.
|
|
125
|
+
r : float
|
|
126
|
+
The continuously compounded risk-free rate.
|
|
127
|
+
q : float
|
|
128
|
+
The continuously compounded dividend yield.
|
|
129
|
+
v0 : float
|
|
130
|
+
The initial variance of the asset's returns.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
CF
|
|
135
|
+
The characteristic function.
|
|
136
|
+
"""
|
|
137
|
+
p = self.params
|
|
138
|
+
kappa, theta, rho, vol_of_vol = (
|
|
139
|
+
p["kappa"],
|
|
140
|
+
p["theta"],
|
|
141
|
+
p["rho"],
|
|
142
|
+
p["vol_of_vol"],
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def phi(u: np.ndarray | complex) -> np.ndarray | complex:
|
|
146
|
+
d = np.sqrt(
|
|
147
|
+
(rho * vol_of_vol * u * 1j - kappa) ** 2
|
|
148
|
+
- (vol_of_vol**2) * (-u * 1j - u**2)
|
|
149
|
+
)
|
|
150
|
+
g = (kappa - rho * vol_of_vol * u * 1j - d) / (
|
|
151
|
+
kappa - rho * vol_of_vol * u * 1j + d
|
|
152
|
+
)
|
|
153
|
+
C = (r - q) * u * 1j * t + (kappa * theta / vol_of_vol**2) * (
|
|
154
|
+
(kappa - rho * vol_of_vol * u * 1j - d) * t
|
|
155
|
+
- 2 * np.log((1 - g * np.exp(-d * t)) / (1 - g))
|
|
156
|
+
)
|
|
157
|
+
D = ((kappa - rho * vol_of_vol * u * 1j - d) / vol_of_vol**2) * (
|
|
158
|
+
(1 - np.exp(-d * t)) / (1 - g * np.exp(-d * t))
|
|
159
|
+
)
|
|
160
|
+
return np.exp(C + D * v0 + 1j * u * np.log(spot))
|
|
161
|
+
|
|
162
|
+
return phi
|
|
163
|
+
|
|
164
|
+
def get_sde_stepper(self) -> Callable:
|
|
165
|
+
"""
|
|
166
|
+
Returns the SDE stepper function for the Heston model.
|
|
167
|
+
|
|
168
|
+
This is not used by the JIT-compiled kernel but is kept for potential
|
|
169
|
+
future use with non-compiled or more complex simulation loops.
|
|
170
|
+
"""
|
|
171
|
+
p = self.params
|
|
172
|
+
kappa, theta, rho, vol_of_vol = (
|
|
173
|
+
p["kappa"],
|
|
174
|
+
p["theta"],
|
|
175
|
+
p["rho"],
|
|
176
|
+
p["vol_of_vol"],
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def stepper(
|
|
180
|
+
log_s_t: np.ndarray,
|
|
181
|
+
v_t: np.ndarray,
|
|
182
|
+
r: float,
|
|
183
|
+
q: float,
|
|
184
|
+
dt: float,
|
|
185
|
+
dw_s: np.ndarray,
|
|
186
|
+
dw_v: np.ndarray,
|
|
187
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
188
|
+
v_t_pos = np.maximum(v_t, 0)
|
|
189
|
+
v_sqrt = np.sqrt(v_t_pos)
|
|
190
|
+
log_s_t_next = log_s_t + (r - q - 0.5 * v_t_pos) * dt + v_sqrt * dw_s
|
|
191
|
+
v_t_next = v_t + kappa * (theta - v_t_pos) * dt + vol_of_vol * v_sqrt * dw_v
|
|
192
|
+
return log_s_t_next, np.maximum(v_t_next, 0) # Reflection scheme
|
|
193
|
+
|
|
194
|
+
return stepper
|
|
195
|
+
|
|
196
|
+
# Abstract Method Implementations
|
|
197
|
+
def _sde_impl(self, **kwargs: Any) -> Callable:
|
|
198
|
+
return self.get_sde_stepper()
|
|
199
|
+
|
|
200
|
+
def _pde_impl(self, **kwargs: Any) -> Any:
|
|
201
|
+
raise NotImplementedError
|
|
202
|
+
|
|
203
|
+
def _closed_form_impl(self, **kwargs: Any) -> Any:
|
|
204
|
+
raise NotImplementedError
|