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.
Files changed (92) hide show
  1. optpricing-2.0.0.dist-info/METADATA +105 -0
  2. optpricing-2.0.0.dist-info/RECORD +92 -0
  3. optpricing-2.0.0.dist-info/WHEEL +5 -0
  4. optpricing-2.0.0.dist-info/entry_points.txt +3 -0
  5. optpricing-2.0.0.dist-info/licenses/LICENSE +9 -0
  6. optpricing-2.0.0.dist-info/top_level.txt +1 -0
  7. quantfin/__init__.py +101 -0
  8. quantfin/atoms/__init__.py +21 -0
  9. quantfin/atoms/bond.py +28 -0
  10. quantfin/atoms/option.py +70 -0
  11. quantfin/atoms/rate.py +46 -0
  12. quantfin/atoms/stock.py +39 -0
  13. quantfin/calibration/__init__.py +25 -0
  14. quantfin/calibration/calibrator.py +155 -0
  15. quantfin/calibration/fit_jump_parameters.py +66 -0
  16. quantfin/calibration/fit_market_params.py +120 -0
  17. quantfin/calibration/iv_surface.py +155 -0
  18. quantfin/calibration/technique_selector.py +47 -0
  19. quantfin/calibration/vectorized_bsm_iv.py +93 -0
  20. quantfin/calibration/vectorized_integration_iv.py +143 -0
  21. quantfin/cli.py +372 -0
  22. quantfin/config.py +57 -0
  23. quantfin/dashboard/__init__.py +14 -0
  24. quantfin/dashboard/app.py +26 -0
  25. quantfin/dashboard/pages/1_Calibration.py +57 -0
  26. quantfin/dashboard/pages/2_Pricer_and_Greeks.py +150 -0
  27. quantfin/dashboard/pages/3_Financial_Tools.py +69 -0
  28. quantfin/dashboard/pages/__init__.py +0 -0
  29. quantfin/dashboard/plots.py +157 -0
  30. quantfin/dashboard/service.py +176 -0
  31. quantfin/dashboard/widgets.py +87 -0
  32. quantfin/data/__init__.py +26 -0
  33. quantfin/data/historical_manager.py +86 -0
  34. quantfin/data/market_data_manager.py +236 -0
  35. quantfin/models/__init__.py +54 -0
  36. quantfin/models/base/__init__.py +16 -0
  37. quantfin/models/base/base_model.py +198 -0
  38. quantfin/models/base/validators.py +95 -0
  39. quantfin/models/bates.py +192 -0
  40. quantfin/models/blacks_approx.py +134 -0
  41. quantfin/models/bsm.py +262 -0
  42. quantfin/models/cev.py +109 -0
  43. quantfin/models/cgmy.py +189 -0
  44. quantfin/models/cir.py +90 -0
  45. quantfin/models/dupire_local.py +67 -0
  46. quantfin/models/heston.py +204 -0
  47. quantfin/models/hyperbolic.py +170 -0
  48. quantfin/models/kou.py +148 -0
  49. quantfin/models/merton_jump.py +225 -0
  50. quantfin/models/nig.py +180 -0
  51. quantfin/models/perpetual_put.py +115 -0
  52. quantfin/models/sabr.py +64 -0
  53. quantfin/models/sabr_jump.py +68 -0
  54. quantfin/models/vasicek.py +88 -0
  55. quantfin/models/vg.py +178 -0
  56. quantfin/parity/__init__.py +13 -0
  57. quantfin/parity/implied_rate.py +110 -0
  58. quantfin/parity/parity_model.py +178 -0
  59. quantfin/techniques/__init__.py +31 -0
  60. quantfin/techniques/base/__init__.py +21 -0
  61. quantfin/techniques/base/base_technique.py +54 -0
  62. quantfin/techniques/base/greek_mixin.py +265 -0
  63. quantfin/techniques/base/iv_mixin.py +116 -0
  64. quantfin/techniques/base/lattice_technique.py +129 -0
  65. quantfin/techniques/base/pricing_result.py +27 -0
  66. quantfin/techniques/base/random_utils.py +31 -0
  67. quantfin/techniques/closed_form.py +296 -0
  68. quantfin/techniques/crr.py +59 -0
  69. quantfin/techniques/fft.py +178 -0
  70. quantfin/techniques/integration.py +202 -0
  71. quantfin/techniques/kernels/__init__.py +37 -0
  72. quantfin/techniques/kernels/lattice_kernels.py +286 -0
  73. quantfin/techniques/kernels/mc_kernels.py +337 -0
  74. quantfin/techniques/leisen_reimer.py +59 -0
  75. quantfin/techniques/monte_carlo.py +259 -0
  76. quantfin/techniques/pde.py +155 -0
  77. quantfin/techniques/topm.py +59 -0
  78. quantfin/workflows/__init__.py +14 -0
  79. quantfin/workflows/backtest_workflow.py +133 -0
  80. quantfin/workflows/configs/__init__.py +51 -0
  81. quantfin/workflows/configs/bates_config.py +25 -0
  82. quantfin/workflows/configs/bsm_config.py +9 -0
  83. quantfin/workflows/configs/cev_config.py +16 -0
  84. quantfin/workflows/configs/cgmy_config.py +18 -0
  85. quantfin/workflows/configs/heston_config.py +21 -0
  86. quantfin/workflows/configs/hyperbolic_config.py +18 -0
  87. quantfin/workflows/configs/kou_config.py +16 -0
  88. quantfin/workflows/configs/merton_config.py +18 -0
  89. quantfin/workflows/configs/nig_config.py +13 -0
  90. quantfin/workflows/configs/sabr_config.py +19 -0
  91. quantfin/workflows/configs/vg_config.py +9 -0
  92. quantfin/workflows/daily_workflow.py +150 -0
@@ -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