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/nig.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
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 Normal Inverse Gaussian (NIG) model, a pure-jump Lévy process.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class NIGModel(BaseModel):
|
|
16
|
+
"""Normal Inverse Gaussian (NIG) model, a pure-jump Lévy process."""
|
|
17
|
+
|
|
18
|
+
name: str = "Normal Inverse Gaussian"
|
|
19
|
+
supports_cf: bool = True
|
|
20
|
+
is_pure_levy: bool = True
|
|
21
|
+
|
|
22
|
+
default_params = {"alpha": 15.0, "beta": -5.0, "delta": 0.5}
|
|
23
|
+
|
|
24
|
+
def __init__(self, params: dict[str, float] | None = None):
|
|
25
|
+
"""
|
|
26
|
+
Initializes the NIG model.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
params : dict[str, float] | None, optional
|
|
31
|
+
A dictionary of model parameters. If None, `default_params` are used.
|
|
32
|
+
"""
|
|
33
|
+
super().__init__(params or self.default_params)
|
|
34
|
+
|
|
35
|
+
def _validate_params(self) -> None:
|
|
36
|
+
p = self.params
|
|
37
|
+
req = ["alpha", "beta", "delta"]
|
|
38
|
+
ParamValidator.require(p, req, model=self.name)
|
|
39
|
+
ParamValidator.positive(p, ["alpha", "delta"], model=self.name)
|
|
40
|
+
if not (abs(p["beta"]) < p["alpha"]):
|
|
41
|
+
raise ValueError("NIG params must satisfy |beta| < alpha.")
|
|
42
|
+
|
|
43
|
+
def __eq__(self, other: object) -> bool:
|
|
44
|
+
if not isinstance(other, NIGModel):
|
|
45
|
+
return NotImplemented
|
|
46
|
+
return self.params == other.params
|
|
47
|
+
|
|
48
|
+
def __hash__(self) -> int:
|
|
49
|
+
return hash((self.__class__, tuple(sorted(self.params.items()))))
|
|
50
|
+
|
|
51
|
+
def _cf_impl(
|
|
52
|
+
self,
|
|
53
|
+
*,
|
|
54
|
+
t: float,
|
|
55
|
+
spot: float,
|
|
56
|
+
r: float,
|
|
57
|
+
q: float,
|
|
58
|
+
**_: Any,
|
|
59
|
+
) -> CF:
|
|
60
|
+
"""
|
|
61
|
+
Risk-neutral characteristic function for the log-spot price log(S_t).
|
|
62
|
+
|
|
63
|
+
This is the risk-neutral characteristic function, which includes the
|
|
64
|
+
drift adjustment.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
t : float
|
|
69
|
+
The time to maturity of the option, in years.
|
|
70
|
+
spot : float
|
|
71
|
+
The current price of the underlying asset.
|
|
72
|
+
r : float
|
|
73
|
+
The continuously compounded risk-free rate.
|
|
74
|
+
q : float
|
|
75
|
+
The continuously compounded dividend yield.
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
CF
|
|
80
|
+
The characteristic function.
|
|
81
|
+
"""
|
|
82
|
+
p = self.params
|
|
83
|
+
alpha, beta, delta = p["alpha"], p["beta"], p["delta"]
|
|
84
|
+
compensator = delta * (
|
|
85
|
+
np.sqrt(alpha**2 - (beta + 1) ** 2) - np.sqrt(alpha**2 - beta**2)
|
|
86
|
+
)
|
|
87
|
+
drift = r - q + compensator
|
|
88
|
+
|
|
89
|
+
def phi(u: np.ndarray | complex) -> np.ndarray | complex:
|
|
90
|
+
term1 = 1j * u * (np.log(spot) + drift * t)
|
|
91
|
+
term2 = (
|
|
92
|
+
delta
|
|
93
|
+
* t
|
|
94
|
+
* (
|
|
95
|
+
np.sqrt(alpha**2 - beta**2)
|
|
96
|
+
- np.sqrt(alpha**2 - (beta + 1j * u) ** 2)
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
return np.exp(term1 + term2)
|
|
100
|
+
|
|
101
|
+
return phi
|
|
102
|
+
|
|
103
|
+
def raw_cf(self, *, t: float) -> Callable:
|
|
104
|
+
"""
|
|
105
|
+
Returns the CF of the raw NIG 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
|
+
alpha, beta, delta = p["alpha"], p["beta"], p["delta"]
|
|
122
|
+
|
|
123
|
+
def phi_raw(u: np.ndarray | complex) -> np.ndarray | complex:
|
|
124
|
+
term = (
|
|
125
|
+
delta
|
|
126
|
+
* t
|
|
127
|
+
* (
|
|
128
|
+
np.sqrt(alpha**2 - beta**2)
|
|
129
|
+
- np.sqrt(alpha**2 - (beta + 1j * u) ** 2)
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
return np.exp(term)
|
|
133
|
+
|
|
134
|
+
return phi_raw
|
|
135
|
+
|
|
136
|
+
def sample_terminal_log_return(
|
|
137
|
+
self,
|
|
138
|
+
T: float,
|
|
139
|
+
size: int,
|
|
140
|
+
rng: np.random.Generator,
|
|
141
|
+
) -> np.ndarray:
|
|
142
|
+
"""
|
|
143
|
+
Generates exact samples of the terminal log-return for the NIG process.
|
|
144
|
+
|
|
145
|
+
This leverages the representation of the NIG process as a Brownian
|
|
146
|
+
motion with drift, time-changed by an Inverse Gaussian process.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
T : float
|
|
151
|
+
The time to maturity, in years.
|
|
152
|
+
size : int
|
|
153
|
+
The number of samples to generate.
|
|
154
|
+
rng : np.random.Generator
|
|
155
|
+
A random number generator instance for reproducibility.
|
|
156
|
+
|
|
157
|
+
Returns
|
|
158
|
+
-------
|
|
159
|
+
np.ndarray
|
|
160
|
+
An array of simulated terminal log-returns.
|
|
161
|
+
"""
|
|
162
|
+
from scipy.stats import invgauss
|
|
163
|
+
|
|
164
|
+
p = self.params
|
|
165
|
+
alpha, beta, delta = p["alpha"], p["beta"], p["delta"]
|
|
166
|
+
mu_ig = delta * T / np.sqrt(alpha**2 - beta**2)
|
|
167
|
+
ig_time = invgauss.rvs(mu=mu_ig, size=size, random_state=rng)
|
|
168
|
+
bm_drift = beta * ig_time
|
|
169
|
+
bm_diffusion = np.sqrt(ig_time) * rng.standard_normal(size=size)
|
|
170
|
+
return bm_drift + bm_diffusion
|
|
171
|
+
|
|
172
|
+
# Abstract Method Implementations
|
|
173
|
+
def _sde_impl(self, **kwargs: Any) -> Any:
|
|
174
|
+
raise NotImplementedError("NIG is a pure Lévy process, use terminal sampling.")
|
|
175
|
+
|
|
176
|
+
def _pde_impl(self, **kwargs: Any) -> Any:
|
|
177
|
+
raise NotImplementedError
|
|
178
|
+
|
|
179
|
+
def _closed_form_impl(self, **kwargs: Any) -> Any:
|
|
180
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,115 @@
|
|
|
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 a model for pricing a perpetual American put option.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PerpetualPutModel(BaseModel):
|
|
14
|
+
"""
|
|
15
|
+
Provides a closed-form price for a perpetual American put option.
|
|
16
|
+
|
|
17
|
+
A perpetual option has no expiry date. This model assumes the holder will
|
|
18
|
+
exercise optimally. The risk-free rate and volatility are considered
|
|
19
|
+
intrinsic parameters of the model itself.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
name: str = "Perpetual Put"
|
|
23
|
+
has_closed_form: bool = True
|
|
24
|
+
|
|
25
|
+
default_params = {"sigma": 0.20, "rate": 0.08}
|
|
26
|
+
|
|
27
|
+
def __init__(self, params: dict[str, float]) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Initializes the model with its parameters.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
params : dict[str, float] | None, optional
|
|
34
|
+
A dictionary requiring 'sigma' and 'rate'. If None, `default_params`
|
|
35
|
+
are used.
|
|
36
|
+
"""
|
|
37
|
+
super().__init__(params=params)
|
|
38
|
+
|
|
39
|
+
def _validate_params(self) -> None:
|
|
40
|
+
"""Validates that 'sigma' and 'rate' are present and positive."""
|
|
41
|
+
ParamValidator.require(self.params, ["sigma", "rate"], model=self.name)
|
|
42
|
+
ParamValidator.positive(self.params, ["sigma", "rate"], model=self.name)
|
|
43
|
+
|
|
44
|
+
def __eq__(self, other: object) -> bool:
|
|
45
|
+
if not isinstance(other, PerpetualPutModel):
|
|
46
|
+
return NotImplemented
|
|
47
|
+
return self.params == other.params
|
|
48
|
+
|
|
49
|
+
def __hash__(self) -> int:
|
|
50
|
+
return hash((self.__class__, tuple(sorted(self.params.items()))))
|
|
51
|
+
|
|
52
|
+
def _closed_form_impl(
|
|
53
|
+
self,
|
|
54
|
+
*,
|
|
55
|
+
spot: float,
|
|
56
|
+
strike: float,
|
|
57
|
+
q: float,
|
|
58
|
+
**_: Any,
|
|
59
|
+
) -> float:
|
|
60
|
+
"""
|
|
61
|
+
Calculates the price of a perpetual American put.
|
|
62
|
+
|
|
63
|
+
Note: This model uses the 'rate' from its own parameters (`self.params`),
|
|
64
|
+
ignoring the risk-free rate passed in from the pricing technique. This
|
|
65
|
+
is a specific design choice for this model. The `t` and `call`
|
|
66
|
+
parameters are also ignored as they are not applicable.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
spot : float
|
|
71
|
+
The current price of the underlying asset.
|
|
72
|
+
strike : float
|
|
73
|
+
The strike price of the option.
|
|
74
|
+
q : float
|
|
75
|
+
The continuously compounded dividend yield of the asset.
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
float
|
|
80
|
+
The price of the perpetual American put.
|
|
81
|
+
"""
|
|
82
|
+
# uses its own intrinsic rate, not the one from the Rate object.
|
|
83
|
+
r = self.params["rate"]
|
|
84
|
+
sigma = self.params["sigma"]
|
|
85
|
+
vol_sq = sigma**2
|
|
86
|
+
|
|
87
|
+
# negative root of the characteristic quadratic equation
|
|
88
|
+
b = r - q - 0.5 * vol_sq
|
|
89
|
+
gamma = (-b - math.sqrt(b**2 + 2 * r * vol_sq)) / vol_sq
|
|
90
|
+
|
|
91
|
+
# Calculate the optimal exercise boundary (S*)
|
|
92
|
+
s_star = strike * gamma / (gamma - 1.0)
|
|
93
|
+
|
|
94
|
+
if spot <= s_star:
|
|
95
|
+
# If it is optimal to exercise, the value is the intrinsic value.
|
|
96
|
+
return strike - spot
|
|
97
|
+
else:
|
|
98
|
+
# Otherwise, the value is given by the standard formula.
|
|
99
|
+
return (strike - s_star) * (spot / s_star) ** gamma
|
|
100
|
+
|
|
101
|
+
# Abstract Method Implementations
|
|
102
|
+
def _cf_impl(
|
|
103
|
+
self,
|
|
104
|
+
*args: Any,
|
|
105
|
+
**kwargs: Any,
|
|
106
|
+
) -> Any:
|
|
107
|
+
raise NotImplementedError(
|
|
108
|
+
f"{self.name} does not support a characteristic function."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def _sde_impl(self) -> Any:
|
|
112
|
+
raise NotImplementedError(f"{self.name} does not support SDE sampling.")
|
|
113
|
+
|
|
114
|
+
def _pde_impl(self) -> Any:
|
|
115
|
+
raise NotImplementedError(f"{self.name} does not support PDE solving.")
|
quantfin/models/sabr.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from quantfin.models.base import BaseModel, ParamValidator
|
|
6
|
+
|
|
7
|
+
__doc__ = """
|
|
8
|
+
Defines the Stochastic Alpha, Beta, Rho (SABR) model.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SABRModel(BaseModel):
|
|
13
|
+
"""
|
|
14
|
+
Stochastic Alpha, Beta, Rho (SABR) model from Hagan et al. (2002).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
name: str = "SABR"
|
|
18
|
+
supports_sde: bool = True
|
|
19
|
+
has_variance_process: bool = True
|
|
20
|
+
is_sabr: bool = True
|
|
21
|
+
default_params = {"alpha": 0.5, "beta": 0.8, "rho": -0.6}
|
|
22
|
+
|
|
23
|
+
def __init__(self, params: dict[str, float] | None = None):
|
|
24
|
+
"""
|
|
25
|
+
Initializes the SABR 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
|
+
"""Validates that 'alpha', 'beta', and 'rho' are present and positive."""
|
|
36
|
+
p = self.params
|
|
37
|
+
req = ["alpha", "beta", "rho"]
|
|
38
|
+
ParamValidator.require(p, req, model=self.name)
|
|
39
|
+
ParamValidator.positive(p, ["alpha"], model=self.name)
|
|
40
|
+
ParamValidator.bounded(p, "beta", 0.0, 1.0, model=self.name)
|
|
41
|
+
ParamValidator.bounded(p, "rho", -1.0, 1.0, model=self.name)
|
|
42
|
+
|
|
43
|
+
def __eq__(self, other: object) -> bool:
|
|
44
|
+
if not isinstance(other, SABRModel):
|
|
45
|
+
return NotImplemented
|
|
46
|
+
return self.params == other.params
|
|
47
|
+
|
|
48
|
+
def __hash__(self) -> int:
|
|
49
|
+
return hash((self.__class__, tuple(sorted(self.params.items()))))
|
|
50
|
+
|
|
51
|
+
# Abstract Method Implementations
|
|
52
|
+
def _sde_impl(self, **kwargs: Any) -> Any:
|
|
53
|
+
raise NotImplementedError(
|
|
54
|
+
"SABR uses a specialized kernel, not a generic stepper."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def _cf_impl(self, **kwargs: Any) -> Any:
|
|
58
|
+
raise NotImplementedError
|
|
59
|
+
|
|
60
|
+
def _pde_impl(self, **kwargs: Any) -> Any:
|
|
61
|
+
raise NotImplementedError
|
|
62
|
+
|
|
63
|
+
def _closed_form_impl(self, **kwargs: Any) -> Any:
|
|
64
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from quantfin.models.base import BaseModel, ParamValidator
|
|
6
|
+
|
|
7
|
+
__doc__ = """
|
|
8
|
+
Defines the SABR model with an added log-normal jump component.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SABRJumpModel(BaseModel):
|
|
13
|
+
"""SABR model with an added log-normal jump component on the spot process."""
|
|
14
|
+
|
|
15
|
+
name: str = "SABR with Jumps"
|
|
16
|
+
supports_sde: bool = True
|
|
17
|
+
has_variance_process: bool = True
|
|
18
|
+
has_jumps: bool = True
|
|
19
|
+
is_sabr: bool = True
|
|
20
|
+
|
|
21
|
+
default_params = {
|
|
22
|
+
"alpha": 0.5,
|
|
23
|
+
"beta": 0.8,
|
|
24
|
+
"rho": -0.6,
|
|
25
|
+
"lambda": 0.4,
|
|
26
|
+
"mu_j": -0.1,
|
|
27
|
+
"sigma_j": 0.15,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def __init__(self, params: dict[str, float] | None = None):
|
|
31
|
+
"""
|
|
32
|
+
Initializes the SABR with Jumps model.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
params : dict[str, float] | None, optional
|
|
37
|
+
A dictionary of model parameters. If None, `default_params` are used.
|
|
38
|
+
Defaults to None.
|
|
39
|
+
"""
|
|
40
|
+
super().__init__(params or self.default_params)
|
|
41
|
+
|
|
42
|
+
def _validate_params(self) -> None:
|
|
43
|
+
p = self.params
|
|
44
|
+
req = ["alpha", "beta", "rho", "lambda", "mu_j", "sigma_j"]
|
|
45
|
+
ParamValidator.require(p, req, model=self.name)
|
|
46
|
+
|
|
47
|
+
def __eq__(self, other: object) -> bool:
|
|
48
|
+
if not isinstance(other, SABRJumpModel):
|
|
49
|
+
return NotImplemented
|
|
50
|
+
return self.params == other.params
|
|
51
|
+
|
|
52
|
+
def __hash__(self) -> int:
|
|
53
|
+
return hash((self.__class__, tuple(sorted(self.params.items()))))
|
|
54
|
+
|
|
55
|
+
# Abstract Method Implementations
|
|
56
|
+
def _sde_impl(self, **kwargs: Any) -> Any:
|
|
57
|
+
raise NotImplementedError(
|
|
58
|
+
"SABR uses a specialized kernel, not a generic stepper."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def _cf_impl(self, **kwargs: Any) -> Any:
|
|
62
|
+
raise NotImplementedError
|
|
63
|
+
|
|
64
|
+
def _pde_impl(self, **kwargs: Any) -> Any:
|
|
65
|
+
raise NotImplementedError
|
|
66
|
+
|
|
67
|
+
def _closed_form_impl(self, **kwargs: Any) -> Any:
|
|
68
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,88 @@
|
|
|
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 Vasicek (1977) mean-reverting interest rate model.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VasicekModel(BaseModel):
|
|
14
|
+
"""
|
|
15
|
+
Vasicek mean-reverting short rate model.
|
|
16
|
+
dr_t = kappa * (theta - r_t) * dt + sigma * dW_t
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
name: str = "Vasicek"
|
|
20
|
+
has_closed_form: bool = True
|
|
21
|
+
|
|
22
|
+
default_params = {"kappa": 0.86, "theta": 0.09, "sigma": 0.02}
|
|
23
|
+
|
|
24
|
+
def __init__(self, params: dict[str, float] | None = None):
|
|
25
|
+
"""
|
|
26
|
+
Initializes the Vasicek model.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
params : dict[str, float] | None, optional
|
|
31
|
+
A dictionary of model parameters. If None, `default_params` are used.
|
|
32
|
+
Defaults to None.
|
|
33
|
+
"""
|
|
34
|
+
super().__init__(params or self.default_params)
|
|
35
|
+
|
|
36
|
+
def _validate_params(self) -> None:
|
|
37
|
+
"""Validates that 'kappa', 'theta', and 'sigma' are present and positive."""
|
|
38
|
+
p = self.params
|
|
39
|
+
req = ["kappa", "theta", "sigma"]
|
|
40
|
+
ParamValidator.require(p, req, model=self.name)
|
|
41
|
+
ParamValidator.positive(p, ["kappa", "theta", "sigma"], model=self.name)
|
|
42
|
+
|
|
43
|
+
def _closed_form_impl(
|
|
44
|
+
self,
|
|
45
|
+
*,
|
|
46
|
+
spot: float,
|
|
47
|
+
t: float,
|
|
48
|
+
**_: Any,
|
|
49
|
+
) -> float:
|
|
50
|
+
"""
|
|
51
|
+
Calculates the price of a Zero-Coupon Bond.
|
|
52
|
+
|
|
53
|
+
Note: Re-interprets 'spot' as the initial short rate r0 and 't' as
|
|
54
|
+
the bond's maturity T. Ignores other option-specific parameters.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
spot : float
|
|
59
|
+
The initial short rate, r0.
|
|
60
|
+
t : float
|
|
61
|
+
The maturity of the bond, in years.
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
float
|
|
66
|
+
The price of the zero-coupon bond.
|
|
67
|
+
"""
|
|
68
|
+
r0, T = spot, t
|
|
69
|
+
p = self.params
|
|
70
|
+
kappa, theta, sigma = p["kappa"], p["theta"], p["sigma"]
|
|
71
|
+
|
|
72
|
+
B = (1 / kappa) * (1 - math.exp(-kappa * T))
|
|
73
|
+
A_log = (theta - sigma**2 / (2 * kappa**2)) * (B - T) - (
|
|
74
|
+
sigma**2 / (4 * kappa)
|
|
75
|
+
) * B**2
|
|
76
|
+
|
|
77
|
+
price = math.exp(A_log - B * r0)
|
|
78
|
+
return price
|
|
79
|
+
|
|
80
|
+
# Abstract Method Implementations
|
|
81
|
+
def _sde_impl(self, **kwargs: Any) -> Any:
|
|
82
|
+
raise NotImplementedError
|
|
83
|
+
|
|
84
|
+
def _cf_impl(self, **kwargs: Any) -> Any:
|
|
85
|
+
raise NotImplementedError
|
|
86
|
+
|
|
87
|
+
def _pde_impl(self, **kwargs: Any) -> Any:
|
|
88
|
+
raise NotImplementedError
|
quantfin/models/vg.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
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 Variance Gamma (VG) model, a pure-jump Lévy process.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class VarianceGammaModel(BaseModel):
|
|
16
|
+
"""Variance Gamma (VG) model, a pure-jump Lévy process."""
|
|
17
|
+
|
|
18
|
+
name: str = "Variance Gamma"
|
|
19
|
+
supports_cf: bool = True
|
|
20
|
+
is_pure_levy: bool = True
|
|
21
|
+
|
|
22
|
+
default_params = {"sigma": 0.2, "nu": 0.1, "theta": -0.14}
|
|
23
|
+
param_defs = {
|
|
24
|
+
"sigma": {
|
|
25
|
+
"label": "Volatility",
|
|
26
|
+
"default": 0.2,
|
|
27
|
+
"min": 0.01,
|
|
28
|
+
"max": 2.0,
|
|
29
|
+
"step": 0.01,
|
|
30
|
+
},
|
|
31
|
+
"nu": {
|
|
32
|
+
"label": "Variance Rate",
|
|
33
|
+
"default": 0.1,
|
|
34
|
+
"min": 0.001,
|
|
35
|
+
"max": 2.0,
|
|
36
|
+
"step": 0.01,
|
|
37
|
+
},
|
|
38
|
+
"theta": {
|
|
39
|
+
"label": "Drift",
|
|
40
|
+
"default": -0.14,
|
|
41
|
+
"min": -2.0,
|
|
42
|
+
"max": 2.0,
|
|
43
|
+
"step": 0.05,
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
def __init__(self, params: dict[str, float] | None = None):
|
|
48
|
+
"""
|
|
49
|
+
Initializes the Variance Gamma (VG) model.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
params : dict[str, float]
|
|
54
|
+
A dictionary of model parameters.
|
|
55
|
+
"""
|
|
56
|
+
super().__init__(params or self.default_params)
|
|
57
|
+
|
|
58
|
+
def _validate_params(self) -> None:
|
|
59
|
+
p = self.params
|
|
60
|
+
req = ["sigma", "nu", "theta"]
|
|
61
|
+
ParamValidator.require(p, req, model=self.name)
|
|
62
|
+
ParamValidator.positive(p, ["sigma", "nu"], model=self.name)
|
|
63
|
+
|
|
64
|
+
def __eq__(self, other: object) -> bool:
|
|
65
|
+
if not isinstance(other, VarianceGammaModel):
|
|
66
|
+
return NotImplemented
|
|
67
|
+
return self.params == other.params
|
|
68
|
+
|
|
69
|
+
def __hash__(self) -> int:
|
|
70
|
+
return hash((self.__class__, tuple(sorted(self.params.items()))))
|
|
71
|
+
|
|
72
|
+
def _cf_impl(
|
|
73
|
+
self,
|
|
74
|
+
*,
|
|
75
|
+
t: float,
|
|
76
|
+
spot: float,
|
|
77
|
+
r: float,
|
|
78
|
+
q: float,
|
|
79
|
+
**_: Any,
|
|
80
|
+
) -> CF:
|
|
81
|
+
"""
|
|
82
|
+
Variance Gamma characteristic function for the log-spot price log(S_t).
|
|
83
|
+
|
|
84
|
+
This is the risk-neutral characteristic function, which includes the
|
|
85
|
+
drift adjustment.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
t : float
|
|
90
|
+
The time to maturity of the option, in years.
|
|
91
|
+
spot : float
|
|
92
|
+
The current price of the underlying asset.
|
|
93
|
+
r : float
|
|
94
|
+
The continuously compounded risk-free rate.
|
|
95
|
+
q : float
|
|
96
|
+
The continuously compounded dividend yield.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
CF
|
|
101
|
+
The characteristic function.
|
|
102
|
+
"""
|
|
103
|
+
compensator = np.log(self.raw_cf(t=1.0)(-1j))
|
|
104
|
+
drift = r - q - compensator
|
|
105
|
+
|
|
106
|
+
def phi(u: np.ndarray | complex) -> np.ndarray | complex:
|
|
107
|
+
return self.raw_cf(t=t)(u) * np.exp(1j * u * (np.log(spot) + drift * t))
|
|
108
|
+
|
|
109
|
+
return phi
|
|
110
|
+
|
|
111
|
+
def raw_cf(self, *, t: float) -> Callable:
|
|
112
|
+
"""
|
|
113
|
+
Returns the raw characteristic function of the VG process itself.
|
|
114
|
+
|
|
115
|
+
This function represents the characteristic function of the process
|
|
116
|
+
before the risk-neutral drift adjustment is applied.
|
|
117
|
+
|
|
118
|
+
Parameters
|
|
119
|
+
----------
|
|
120
|
+
t : float
|
|
121
|
+
The time horizon.
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
Callable
|
|
126
|
+
The raw characteristic function.
|
|
127
|
+
"""
|
|
128
|
+
p = self.params
|
|
129
|
+
sigma, nu, theta = p["sigma"], p["nu"], p["theta"]
|
|
130
|
+
|
|
131
|
+
def phi_raw(u: np.ndarray | complex) -> np.ndarray | complex:
|
|
132
|
+
return (1 - 1j * u * theta * nu + 0.5 * u**2 * sigma**2 * nu) ** (-t / nu)
|
|
133
|
+
|
|
134
|
+
return phi_raw
|
|
135
|
+
|
|
136
|
+
def sample_terminal_log_return(
|
|
137
|
+
self,
|
|
138
|
+
T: float,
|
|
139
|
+
size: int,
|
|
140
|
+
rng: np.random.Generator,
|
|
141
|
+
) -> np.ndarray:
|
|
142
|
+
"""
|
|
143
|
+
Generates exact samples of the terminal log-return for the VG process.
|
|
144
|
+
|
|
145
|
+
This leverages the representation of the VG process as a Brownian
|
|
146
|
+
motion with drift, time-changed by a Gamma process.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
T : float
|
|
151
|
+
The time to maturity, in years.
|
|
152
|
+
size : int
|
|
153
|
+
The number of samples to generate.
|
|
154
|
+
rng : np.random.Generator
|
|
155
|
+
A random number generator instance for reproducibility.
|
|
156
|
+
|
|
157
|
+
Returns
|
|
158
|
+
-------
|
|
159
|
+
np.ndarray
|
|
160
|
+
An array of simulated terminal log-returns.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
p = self.params
|
|
164
|
+
sigma, nu, theta = p["sigma"], p["nu"], p["theta"]
|
|
165
|
+
gamma_time = rng.gamma(shape=T / nu, scale=nu, size=size)
|
|
166
|
+
bm_drift = theta * gamma_time
|
|
167
|
+
bm_diffusion = sigma * np.sqrt(gamma_time) * rng.standard_normal(size=size)
|
|
168
|
+
return bm_drift + bm_diffusion
|
|
169
|
+
|
|
170
|
+
# Abstract Method Implementations
|
|
171
|
+
def _sde_impl(self, **kwargs: Any) -> Any:
|
|
172
|
+
raise NotImplementedError
|
|
173
|
+
|
|
174
|
+
def _pde_impl(self, **kwargs: Any) -> Any:
|
|
175
|
+
raise NotImplementedError
|
|
176
|
+
|
|
177
|
+
def _closed_form_impl(self, **kwargs: Any) -> Any:
|
|
178
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
__doc__ = """
|
|
4
|
+
The `parity` package provides models and utilities based on put-call parity.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .implied_rate import ImpliedRateModel
|
|
8
|
+
from .parity_model import ParityModel
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ParityModel",
|
|
12
|
+
"ImpliedRateModel",
|
|
13
|
+
]
|