stochvolmodels 1.1.5__tar.gz → 1.1.7__tar.gz
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.
- {stochvolmodels-1.1.5/stochvolmodels.egg-info → stochvolmodels-1.1.7}/PKG-INFO +3 -2
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/pyproject.toml +4 -3
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/requirements.txt +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/logsv_pricer.py +10 -8
- stochvolmodels-1.1.7/stochvolmodels/pricers/rough_logsv/split_simulation.py +482 -0
- stochvolmodels-1.1.7/stochvolmodels/tests/rough_logsv_perf.py +302 -0
- stochvolmodels-1.1.7/stochvolmodels/tests/test_rough_logsv_pricer_regression/test_rough_logsv_pricer_pricing_regression.yml +50 -0
- stochvolmodels-1.1.7/stochvolmodels/tests/test_rough_logsv_pricer_regression.py +50 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7/stochvolmodels.egg-info}/PKG-INFO +3 -2
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels.egg-info/SOURCES.txt +3 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels.egg-info/requires.txt +2 -1
- stochvolmodels-1.1.5/stochvolmodels/pricers/rough_logsv/split_simulation.py +0 -262
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/.gitignore +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/LICENSE.txt +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/MANIFEST.in +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/README.md +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/forward_var/calibrate_forward_var.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/il_hedging/README.md +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/il_hedging/logsv_figures.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/il_hedging/run_logsv_for_il_payoff.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/inverse_options/README.md +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/inverse_options/compare_net_delta.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/logsv_model_wtih_quadratic_drift/README.md +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/logsv_model_wtih_quadratic_drift/article_figures.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/logsv_model_wtih_quadratic_drift/calibrations.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/logsv_model_wtih_quadratic_drift/compare_admis_reg.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/logsv_model_wtih_quadratic_drift/model_fit_to_options_timeseries.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/logsv_model_wtih_quadratic_drift/moments_vol_qvar.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/logsv_model_wtih_quadratic_drift/ode_sol_in_time.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/logsv_model_wtih_quadratic_drift/steady_state_pdf.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/logsv_model_wtih_quadratic_drift/vol_drift.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/risk_premia_gmm/check_kernel.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/risk_premia_gmm/gmm_slides.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/risk_premia_gmm/plot_gmm.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/risk_premia_gmm/q_kernel.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/risk_premia_gmm/run_gmm_fit.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/sv_for_factor_hjm/README.md +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/sv_for_factor_hjm/calibration_fig_5_6_7.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/sv_for_factor_hjm/calibration_fig_8_9.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/t_distribution/illustrations.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/t_distribution/market_data_fit.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/t_distribution/mc_pricer_with_kernel.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/volatility_models/README.md +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/volatility_models/article_figures.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/volatility_models/autocorr_fit.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/volatility_models/load_data.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/volatility_models/ss_distribution_fit.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/volatility_models/vol_beta.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/setup.cfg +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/__init__.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/data/__init__.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/data/fetch_option_chain.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/data/option_chain.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/data/test_option_chain.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/examples/quick_run_lognormal_sv_pricer.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/examples/run_hawkes_pricer.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/examples/run_heston.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/examples/run_heston_sv_pricer.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/examples/run_lognormal_sv_pricer.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/examples/run_pricing_options_on_qvar.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/__init__.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/analytic/__init__.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/analytic/bachelier.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/analytic/bsm.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/analytic/tdist.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/factor_hjm/double_exp_pricer.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/factor_hjm/factor_hjm_pricer.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/factor_hjm/rate_affine_expansion.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/factor_hjm/rate_core.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/factor_hjm/rate_evaluate.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/factor_hjm/rate_factor_basis.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/factor_hjm/rate_logsv_ivols.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/factor_hjm/rate_logsv_params.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/factor_hjm/rate_logsv_pricer.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/gmm_pricer.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/hawkes_jd_pricer.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/heston_pricer.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/logsv/__init__.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/logsv/affine_expansion.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/logsv/logsv_params.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/logsv/vol_moments_ode.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/model_pricer.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/rough_logsv/RoughKernel.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/rough_logsv/expm.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/rough_logsv/test_kernel_approx.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/tdist_pricer.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/tests/__init__.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/tests/bsm_mgf_pricer.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/tests/qv_pricer.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/utils/__init__.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/utils/config.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/utils/funcs.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/utils/mc_payoffs.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/utils/mgf_pricer.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/utils/plots.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/utils/var_swap_pricer.py +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels.egg-info/dependency_links.txt +0 -0
- {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stochvolmodels
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.7
|
|
4
4
|
Summary: Python implementation of pricing analytics and Monte Carlo simulations for stochastic volatility models including log-normal SV model, Heston
|
|
5
5
|
Author-email: Artur Sepp <artursepp@gmail.com>
|
|
6
6
|
Maintainer-email: Artur Sepp <artursepp@gmail.com>, Parviz Rakhmonov <ParvizRZ@gmail.com>
|
|
@@ -727,9 +727,10 @@ Requires-Dist: jupyterlab>=3.0.0; extra == "jupyter"
|
|
|
727
727
|
Requires-Dist: ipykernel>=6.0.0; extra == "jupyter"
|
|
728
728
|
Requires-Dist: ipywidgets>=8.0.0; extra == "jupyter"
|
|
729
729
|
Provides-Extra: dev
|
|
730
|
-
Requires-Dist: pytest>=
|
|
730
|
+
Requires-Dist: pytest>=8.4.2; extra == "dev"
|
|
731
731
|
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
732
732
|
Requires-Dist: pytest-mock>=3.10.0; extra == "dev"
|
|
733
|
+
Requires-Dist: pytest-regressions>=2.8.3; extra == "dev"
|
|
733
734
|
Requires-Dist: pytest-xdist>=3.0.0; extra == "dev"
|
|
734
735
|
Requires-Dist: black>=22.0.0; extra == "dev"
|
|
735
736
|
Requires-Dist: flake8>=5.0.0; extra == "dev"
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "stochvolmodels"
|
|
10
|
-
version = "1.1.
|
|
10
|
+
version = "1.1.7"
|
|
11
11
|
description = "Python implementation of pricing analytics and Monte Carlo simulations for stochastic volatility models including log-normal SV model, Heston"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
license = {file = "LICENSE.txt"}
|
|
@@ -95,9 +95,10 @@ jupyter = [
|
|
|
95
95
|
|
|
96
96
|
# Development tools and testing
|
|
97
97
|
dev = [
|
|
98
|
-
"pytest>=
|
|
98
|
+
"pytest>=8.4.2",
|
|
99
99
|
"pytest-cov>=4.0.0",
|
|
100
100
|
"pytest-mock>=3.10.0",
|
|
101
|
+
"pytest-regressions>=2.8.3",
|
|
101
102
|
"pytest-xdist>=3.0.0",
|
|
102
103
|
"black>=22.0.0",
|
|
103
104
|
"flake8>=5.0.0",
|
|
@@ -276,4 +277,4 @@ exclude = [
|
|
|
276
277
|
"venv",
|
|
277
278
|
".eggs",
|
|
278
279
|
"*.egg",
|
|
279
|
-
]
|
|
280
|
+
]
|
|
Binary file
|
|
@@ -875,12 +875,14 @@ def rough_logsv_mc_chain_pricer_fixed_randoms(ttms: np.ndarray,
|
|
|
875
875
|
weights: np.ndarray,
|
|
876
876
|
nodes: np.ndarray,
|
|
877
877
|
timegrids: List[np.ndarray],
|
|
878
|
-
variable_type: VariableType = VariableType.LOG_RETURN
|
|
878
|
+
variable_type: VariableType = VariableType.LOG_RETURN,
|
|
879
|
+
debug: bool = True
|
|
879
880
|
) -> Tuple[List[np.ndarray], List[np.ndarray]]:
|
|
880
881
|
assert weights.shape == nodes.shape and weights.ndim == 1
|
|
881
882
|
# assert kappa2 == 0.0
|
|
882
883
|
N = nodes.size
|
|
883
|
-
|
|
884
|
+
dtype = weights.dtype
|
|
885
|
+
v0 = np.full((N,), sigma0 / np.sum(weights), dtype=dtype)
|
|
884
886
|
|
|
885
887
|
# need to redenote coefficients
|
|
886
888
|
volvol = np.sqrt(beta ** 2 + orthog_vol ** 2)
|
|
@@ -889,7 +891,9 @@ def rough_logsv_mc_chain_pricer_fixed_randoms(ttms: np.ndarray,
|
|
|
889
891
|
nb_path = Z0.shape[1]
|
|
890
892
|
v0_vec = np.repeat(v0[:, None], nb_path, axis=1)
|
|
891
893
|
v_init = v0_vec.copy()
|
|
892
|
-
log_s0 = 0.0
|
|
894
|
+
log_s0 = dtype.type(0.0)
|
|
895
|
+
weight_vec = np.repeat(weights[:, None], nb_path, axis=1)
|
|
896
|
+
nodes_vec = np.repeat(nodes[:, None], nb_path, axis=1)
|
|
893
897
|
|
|
894
898
|
# outputs as numpy lists
|
|
895
899
|
option_prices_ttm = List()
|
|
@@ -902,12 +906,11 @@ def rough_logsv_mc_chain_pricer_fixed_randoms(ttms: np.ndarray,
|
|
|
902
906
|
nb_steps = timegrid.size - 1
|
|
903
907
|
Z0_ = Z0[:nb_steps]
|
|
904
908
|
Z1_ = Z1[:nb_steps]
|
|
905
|
-
weight_vec = np.repeat(weights[:, None], nb_path,axis=1)
|
|
906
|
-
nodes_vec = np.repeat(nodes[:, None], nb_path,axis=1)
|
|
907
909
|
log_spot_str, vol_str, qv_str = log_spot_full_combined(nodes_vec, weight_vec, v0_vec, theta, kappa1, kappa2, log_s0,
|
|
908
910
|
v_init, rho, volvol, timegrid, nb_path, Z0_, Z1_)
|
|
909
|
-
|
|
910
|
-
|
|
911
|
+
if debug:
|
|
912
|
+
print(f"Number of paths with negative vol: {np.sum(weights @ vol_str < 0.0)}, nan vol: {np.count_nonzero(np.isnan(weights @ vol_str))}")
|
|
913
|
+
print(f"Mean spot Strand: {np.mean(np.exp(log_spot_str))}, nan spots: {np.count_nonzero(np.isnan(log_spot_str))}")
|
|
911
914
|
|
|
912
915
|
option_prices, option_std = compute_mc_vars_payoff(x0=log_spot_str, sigma0=vol_str, qvar0=qv_str,
|
|
913
916
|
ttm=ttm,
|
|
@@ -921,7 +924,6 @@ def rough_logsv_mc_chain_pricer_fixed_randoms(ttms: np.ndarray,
|
|
|
921
924
|
|
|
922
925
|
return option_prices_ttm, option_std_ttm
|
|
923
926
|
|
|
924
|
-
|
|
925
927
|
class LocalTests(Enum):
|
|
926
928
|
CHAIN_PRICER = 1
|
|
927
929
|
SLICE_PRICER = 2
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
from scipy.linalg import expm
|
|
6
|
+
from numba import njit
|
|
7
|
+
from numba import njit, objmode
|
|
8
|
+
|
|
9
|
+
from stochvolmodels.pricers.rough_logsv.expm import batch_expA, batch_invA
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# from stochvolmodels.utils.config import VariableType
|
|
13
|
+
# from RoughKernel import european_rule
|
|
14
|
+
# from stochvolmodels.pricers.logsv_pricer import LogSvParams
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@njit(cache=False, fastmath=True)
|
|
18
|
+
def drift_ode_solve(nodes: np.ndarray, v0: np.ndarray, theta: float, kappa1: float, kappa2: float,
|
|
19
|
+
z0: np.ndarray, weight: np.ndarray, h: float):
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
nodes : (fixed argument) exponents x_i, array of size (n,)
|
|
25
|
+
v0 : (fixed argument) array of size (n, nb_path)
|
|
26
|
+
theta : long-run level, scalar
|
|
27
|
+
kappa1 : linear mean-reversion speed, scalar
|
|
28
|
+
kappa2 : quadratic mean-reversion speed, scalar
|
|
29
|
+
z0 : initial values, array of size (n, nb_path)
|
|
30
|
+
weight : wieghts, array of size (n, )
|
|
31
|
+
h : step size
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
Array of size (n, nb_path)
|
|
36
|
+
"""
|
|
37
|
+
assert nodes.shape == v0.shape == z0.shape == weight.shape
|
|
38
|
+
n = z0.shape[0]
|
|
39
|
+
z0w = np.sum(weight * z0, axis=0)
|
|
40
|
+
g0 = (kappa1 + kappa2*z0w)*(theta-z0w)
|
|
41
|
+
k1 = -nodes * (z0 - v0)
|
|
42
|
+
for j in range(n):
|
|
43
|
+
k1[j] += g0
|
|
44
|
+
k1 *= 0.5 * h
|
|
45
|
+
|
|
46
|
+
zmid = z0 + k1
|
|
47
|
+
zmidw = np.sum(weight * zmid, axis=0)
|
|
48
|
+
gmid = (kappa1 + kappa2 * zmidw) * (theta - zmidw)
|
|
49
|
+
k2 = -nodes * (zmid - v0)
|
|
50
|
+
for j in range(n):
|
|
51
|
+
k2[j] += gmid
|
|
52
|
+
k2 *= h
|
|
53
|
+
Dzh = z0 + k2
|
|
54
|
+
|
|
55
|
+
# s1 = -nodes[:, None] * (z0 - v0) * h + (kappa1 + kappa2 * z0w) * (theta - z0w) * h
|
|
56
|
+
# s2 = -nodes[:, None] * (z0 + 0.5 * s1 - v0) * h + (kappa1 + kappa2 * (z0w + 0.5 * s1)) * (theta - (z0w + 0.5 * s1)) * h
|
|
57
|
+
# Dzh = z0 + s2
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# z1 = -nodes * (z0 - v0) * h + (kappa1 + kappa2 * z0w) * (theta - z0w) * h
|
|
62
|
+
# z2 = -nodes * (z0 + 0.5 * z1 - v0) * h + (kappa1 + kappa2 * (z0w + 0.5 * z1)) * (theta - (z0w + 0.5 * z1)) * h
|
|
63
|
+
# Dzh = z0 + z2
|
|
64
|
+
|
|
65
|
+
# lamda_vec = np.repeat(lamda, n)
|
|
66
|
+
# theta_vec = np.repeat(theta, n)[:, None]
|
|
67
|
+
# diag_x = np.diag(nodes)
|
|
68
|
+
# I = np.identity(n)
|
|
69
|
+
# A = -np.outer(lamda_vec, weight) - diag_x # matrix of size (n, n)
|
|
70
|
+
# b = theta_vec + diag_x @ v0 # vector of size (n, nb_path)
|
|
71
|
+
# # with objmode(eAh='float64[:, ::1]'):
|
|
72
|
+
# eAh = expm(A * h)
|
|
73
|
+
# Dzh = eAh @ z0 + (np.linalg.inv(A) @ (eAh - I)) @ b # vector of size (n, nb_path)
|
|
74
|
+
|
|
75
|
+
return Dzh
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@njit(cache=False, fastmath=True)
|
|
79
|
+
def drift_ode_solve2(nodes: np.ndarray, v0: np.ndarray, theta: float, kappa1: float, kappa2: float,
|
|
80
|
+
z0: np.ndarray, weight: np.ndarray, h: float):
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
nodes : (fixed argument) exponents x_i, array of size (n,)
|
|
86
|
+
v0 : (fixed argument) array of size (n, nb_path)
|
|
87
|
+
theta : long-run level, scalar
|
|
88
|
+
kappa1 : linear mean-reversion speed, scalar
|
|
89
|
+
kappa2 : quadratic mean-reversion speed, scalar
|
|
90
|
+
z0 : initial values, array of size (n, nb_path)
|
|
91
|
+
weight : wieghts, array of size (n, )
|
|
92
|
+
h : step size
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
Array of size (n, nb_path)
|
|
97
|
+
"""
|
|
98
|
+
assert nodes.shape == v0.shape == z0.shape == weight.shape
|
|
99
|
+
n, nb_path = weight.shape
|
|
100
|
+
|
|
101
|
+
# --- k1 ---
|
|
102
|
+
z0w = np.sum(weight * z0, axis=0)
|
|
103
|
+
c1 = (kappa1 + kappa2 * z0w) * (theta - z0w)
|
|
104
|
+
s1 = -nodes * (z0 - v0) + c1
|
|
105
|
+
|
|
106
|
+
# --- k2 ---
|
|
107
|
+
z_tmp = z0 + 0.5 * h * s1
|
|
108
|
+
z1w = np.sum(weight * z_tmp, axis=0)
|
|
109
|
+
c2 = (kappa1 + kappa2 * z1w) * (theta - z1w)
|
|
110
|
+
s2 = -nodes * (z_tmp - v0) + c2
|
|
111
|
+
|
|
112
|
+
# --- k3 ---
|
|
113
|
+
z_tmp = z0 + 0.5 * h * s2
|
|
114
|
+
z2w = np.sum(weight * z_tmp, axis=0)
|
|
115
|
+
c3 = (kappa1 + kappa2 * z2w) * (theta - z2w)
|
|
116
|
+
s3 = -nodes * (z_tmp - v0) + c3
|
|
117
|
+
|
|
118
|
+
# --- k4 ---
|
|
119
|
+
z_tmp = z0 + h * s3
|
|
120
|
+
z3w = np.sum(weight * z_tmp, axis=0)
|
|
121
|
+
c4 = (kappa1 + kappa2 * z3w) * (theta - z3w)
|
|
122
|
+
s4 = -nodes * (z_tmp - v0) + c4
|
|
123
|
+
|
|
124
|
+
zh = z0 + (h / 6.0) * (s1 + 2.0 * s2 + 2.0 * s3 + s4)
|
|
125
|
+
|
|
126
|
+
return zh
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@njit(cache=False, fastmath=True)
|
|
130
|
+
def drift_ode_solve2_f32(nodes: np.ndarray, v0: np.ndarray, theta: float, kappa1: float, kappa2: float,
|
|
131
|
+
z0: np.ndarray, weight: np.ndarray, h: float):
|
|
132
|
+
"""
|
|
133
|
+
Float32-friendly variant of drift_ode_solve2 (avoids float64 literals promoting intermediates).
|
|
134
|
+
"""
|
|
135
|
+
assert nodes.shape == v0.shape == z0.shape == weight.shape
|
|
136
|
+
n, nb_path = weight.shape
|
|
137
|
+
|
|
138
|
+
half = np.float32(0.5)
|
|
139
|
+
two = np.float32(2.0)
|
|
140
|
+
six = np.float32(6.0)
|
|
141
|
+
|
|
142
|
+
z0w = np.sum(weight * z0, axis=0)
|
|
143
|
+
c1 = (kappa1 + kappa2 * z0w) * (theta - z0w)
|
|
144
|
+
s1 = -nodes * (z0 - v0) + c1
|
|
145
|
+
|
|
146
|
+
z_tmp = z0 + (half * h) * s1
|
|
147
|
+
z1w = np.sum(weight * z_tmp, axis=0)
|
|
148
|
+
c2 = (kappa1 + kappa2 * z1w) * (theta - z1w)
|
|
149
|
+
s2 = -nodes * (z_tmp - v0) + c2
|
|
150
|
+
|
|
151
|
+
z_tmp = z0 + (half * h) * s2
|
|
152
|
+
z2w = np.sum(weight * z_tmp, axis=0)
|
|
153
|
+
c3 = (kappa1 + kappa2 * z2w) * (theta - z2w)
|
|
154
|
+
s3 = -nodes * (z_tmp - v0) + c3
|
|
155
|
+
|
|
156
|
+
z_tmp = z0 + h * s3
|
|
157
|
+
z3w = np.sum(weight * z_tmp, axis=0)
|
|
158
|
+
c4 = (kappa1 + kappa2 * z3w) * (theta - z3w)
|
|
159
|
+
s4 = -nodes * (z_tmp - v0) + c4
|
|
160
|
+
|
|
161
|
+
zh = z0 + (h / six) * (s1 + two * s2 + two * s3 + s4)
|
|
162
|
+
|
|
163
|
+
return zh
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@njit(cache=False, fastmath=True)
|
|
167
|
+
def drift_ode_solve3(nodes: np.ndarray, v0: np.ndarray, theta: float, kappa1: float, kappa2: float,
|
|
168
|
+
z0: np.ndarray, weight: np.ndarray, h: float):
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
nodes : (fixed argument) exponents x_i, array of size (n,)
|
|
174
|
+
v0 : (fixed argument) array of size (n, nb_path)
|
|
175
|
+
theta : long-run level, scalar
|
|
176
|
+
kappa1 : linear mean-reversion speed, scalar
|
|
177
|
+
kappa2 : quadratic mean-reversion speed, scalar
|
|
178
|
+
z0 : initial values, array of size (n, nb_path)
|
|
179
|
+
weight : wieghts, array of size (n, )
|
|
180
|
+
h : step size
|
|
181
|
+
|
|
182
|
+
Returns
|
|
183
|
+
-------
|
|
184
|
+
Array of size (n, nb_path)
|
|
185
|
+
"""
|
|
186
|
+
assert nodes.shape == v0.shape == z0.shape == weight.shape
|
|
187
|
+
n, nb_path = weight.shape
|
|
188
|
+
z0w = np.sum(weight * z0, axis=0)
|
|
189
|
+
kappa = kappa1 + kappa2 * z0w
|
|
190
|
+
|
|
191
|
+
b_ = np.zeros((nb_path, n))
|
|
192
|
+
for k in range(n):
|
|
193
|
+
b_[:, k] = kappa * theta + nodes[k] * v0[k]
|
|
194
|
+
|
|
195
|
+
eAh = batch_expA(kappa, nodes.T * h, weight.T * h)
|
|
196
|
+
I = np.identity(n)
|
|
197
|
+
invA = batch_invA(kappa, nodes.T, weight.T)
|
|
198
|
+
tmp2 = np.zeros((nb_path, n, n))
|
|
199
|
+
for i in range(n):
|
|
200
|
+
for j in range(n):
|
|
201
|
+
for k in range(n):
|
|
202
|
+
tmp2[:,i,j] += invA[:,i,k] * (eAh[:, k, j] - I[k,j])
|
|
203
|
+
|
|
204
|
+
Dzh_v1 = np.zeros((nb_path, n))
|
|
205
|
+
for i in range(n):
|
|
206
|
+
for j in range(n):
|
|
207
|
+
Dzh_v1[:, i] += eAh[:, i, j] * z0[j, :] + tmp2[:, i, j] * b_[:, j]
|
|
208
|
+
# for p in range(nb_path):
|
|
209
|
+
# Dzh_v1[p] = eAh[p] @ z0[..., p] + tmp2[p] @ b_[p]
|
|
210
|
+
|
|
211
|
+
# def ff2():
|
|
212
|
+
# lamda = kappa1 + kappa2 * z0w
|
|
213
|
+
# coeff0_vec = np.repeat(kappa1 * theta, n)
|
|
214
|
+
# Dzh_v2 = np.zeros_like(z0)
|
|
215
|
+
# for p in range(nb_path):
|
|
216
|
+
# diag_x = np.diag(nodes[:, p])
|
|
217
|
+
# lamda_vec = np.repeat(lamda[p], n)
|
|
218
|
+
# A = -np.outer(lamda_vec, weight[:, p]) - diag_x # matrix of size (n,n)
|
|
219
|
+
# b = coeff0_vec + diag_x @ v0[:, p]
|
|
220
|
+
# eAh_v2 = expm(A * h)
|
|
221
|
+
# Dzh_v2[:, p] = eAh_v2 @ z0[:, p] + (np.linalg.inv(A) @ (eAh_v2 - I)) @ b
|
|
222
|
+
#
|
|
223
|
+
# return Dzh_v2
|
|
224
|
+
#
|
|
225
|
+
# Dzh_v2 = ff2()
|
|
226
|
+
# diff = np.linalg.norm(Dzh_v2-Dzh_v1.T)
|
|
227
|
+
# assert diff < 1e-12
|
|
228
|
+
|
|
229
|
+
return Dzh_v1.T
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@njit(cache=False, fastmath=True)
|
|
233
|
+
def diffus_sde_solve_f64(y0: np.ndarray, weight: np.ndarray, volvol: float, h: float, nb_path: int,
|
|
234
|
+
z_rand: np.ndarray):
|
|
235
|
+
assert y0.shape == weight.shape and y0.shape[-1] == nb_path
|
|
236
|
+
assert z_rand.shape == (nb_path,)
|
|
237
|
+
weight_sum = np.sum(weight, axis=0)
|
|
238
|
+
volvol_ = volvol * weight_sum
|
|
239
|
+
|
|
240
|
+
yw = np.sum(weight * y0, axis=0)
|
|
241
|
+
|
|
242
|
+
dW = z_rand * np.sqrt(h)
|
|
243
|
+
Yh = yw * np.exp(-0.5 * volvol_ ** 2 * h + volvol_ * dW)
|
|
244
|
+
|
|
245
|
+
Q = 1.0 / weight_sum * (Yh - yw)
|
|
246
|
+
Yh_vec = y0.copy()
|
|
247
|
+
for i in range(Yh_vec.shape[0]):
|
|
248
|
+
Yh_vec[i] += Q
|
|
249
|
+
|
|
250
|
+
return Yh_vec
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@njit(cache=False, fastmath=True)
|
|
254
|
+
def drift_diffus_strand_f64(nodes: np.ndarray, v0: np.ndarray, theta: float, kappa1: float, kappa2: float,
|
|
255
|
+
volvol: float, v_init: np.ndarray, weight: np.ndarray, h: float,
|
|
256
|
+
nb_path: int, z_rand: np.ndarray):
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
Parameters
|
|
260
|
+
----------
|
|
261
|
+
nodes : exponents x_i, array of size (n,)
|
|
262
|
+
v0 : (fixed argument) array of size (n, nb_path)
|
|
263
|
+
theta : long-run level, scaled by lambda, scalar
|
|
264
|
+
kappa1 : linear mean-reversion speed, scalar
|
|
265
|
+
kappa2 : quadratic mean-reversion speed, scalar
|
|
266
|
+
volvol : total volatility. in other words, vartheta. scalar
|
|
267
|
+
v_init : initial values, array of size (n, nb_path)
|
|
268
|
+
weight : (fixed argument) wieghts, array of size (n, )
|
|
269
|
+
h : time step size
|
|
270
|
+
nb_path : number of paths for MC simulation
|
|
271
|
+
z_rand : iid standard normals, shape (nb_path, )
|
|
272
|
+
|
|
273
|
+
Returns
|
|
274
|
+
-------
|
|
275
|
+
|
|
276
|
+
"""
|
|
277
|
+
D_inn = drift_ode_solve2(nodes, v0, theta, kappa1, kappa2, v_init, weight, 0.5 * h)
|
|
278
|
+
S_inn = diffus_sde_solve_f64(D_inn, weight, volvol, h, nb_path, z_rand)
|
|
279
|
+
sol = drift_ode_solve2(nodes, v0, theta, kappa1, kappa2, S_inn, weight, 0.5 * h)
|
|
280
|
+
|
|
281
|
+
return sol
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@njit(cache=False, fastmath=True)
|
|
285
|
+
def log_spot_full_solve2_f64(nodes: np.ndarray, weight: np.ndarray,
|
|
286
|
+
v0: np.ndarray, y0: np.ndarray,
|
|
287
|
+
theta: float, kappa1: float, kappa2: float, log_s: np.ndarray, v: np.ndarray, y: np.ndarray,
|
|
288
|
+
rho: float, volvol: float, h: float, nb_path: int,
|
|
289
|
+
z0: np.ndarray, z1: np.ndarray):
|
|
290
|
+
# raise ValueError
|
|
291
|
+
assert nodes.shape == weight.shape and weight.ndim == 2
|
|
292
|
+
assert v.shape == weight.shape and v.shape[-1] == nb_path
|
|
293
|
+
assert y.shape == (1, nb_path)
|
|
294
|
+
assert log_s.shape == (1, nb_path)
|
|
295
|
+
assert v0.shape == weight.shape and v0.shape[-1] == nb_path
|
|
296
|
+
assert y0.shape == (1, nb_path)
|
|
297
|
+
assert z0.shape == (nb_path,) and z1.shape == (nb_path,)
|
|
298
|
+
|
|
299
|
+
vol_h = drift_diffus_strand_f64(nodes, v0, theta, kappa1, kappa2, volvol, v, weight, h, nb_path, z0)
|
|
300
|
+
w_vol_h = np.sum(weight * vol_h, axis=0)
|
|
301
|
+
idx_bad = np.nonzero(np.logical_or(np.isnan(w_vol_h), w_vol_h <= 0.0))[0]
|
|
302
|
+
vol_h[:, idx_bad] = 1e-6
|
|
303
|
+
|
|
304
|
+
wlam = weight * nodes
|
|
305
|
+
vw = np.sum(weight * v, axis=0)
|
|
306
|
+
volw_h = np.sum(weight * vol_h, axis=0)
|
|
307
|
+
w_inv = 1.0 / np.sum(weight, axis=0)
|
|
308
|
+
|
|
309
|
+
c1 = 0.5
|
|
310
|
+
c2 = 0.5
|
|
311
|
+
rho_comp = np.sqrt(1.0 - rho * rho)
|
|
312
|
+
|
|
313
|
+
sq_vw = np.square(vw)
|
|
314
|
+
sq_vhw = np.square(volw_h)
|
|
315
|
+
|
|
316
|
+
w_lam_vol = np.sum(wlam * v, axis=0)
|
|
317
|
+
w_lam_vol_h = np.sum(wlam * vol_h, axis=0)
|
|
318
|
+
w_lam_v0 = np.sum(wlam * v0, axis=0)
|
|
319
|
+
|
|
320
|
+
term1 = 1.0 / volvol * (((volw_h - vw) / h + c1 * w_lam_vol + c2 * w_lam_vol_h - w_lam_v0) * w_inv
|
|
321
|
+
- kappa1 * theta + (kappa1 - kappa2 * theta) * (c1 * vw + c2 * volw_h)
|
|
322
|
+
+ kappa2 * (c1 * sq_vw + c2 * sq_vhw)) * h
|
|
323
|
+
|
|
324
|
+
term2 = c1 * h * sq_vw + c2 * h * sq_vhw
|
|
325
|
+
log_spot_h = log_s - 0.5 * term2 + rho * term1 + rho_comp * np.sqrt(term2) * z1
|
|
326
|
+
|
|
327
|
+
y_h = y + 0.5 * h * (vw * vw + volw_h * volw_h) # don't need it but keep for plotting
|
|
328
|
+
|
|
329
|
+
return vol_h, y_h, log_spot_h
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@njit(cache=False, fastmath=True)
|
|
333
|
+
def log_spot_full_combined_f64(nodes: np.ndarray, weight: np.ndarray,
|
|
334
|
+
v0: np.ndarray,
|
|
335
|
+
theta: float, kappa1: float, kappa2: float, log_s0: float, v_init: np.ndarray,
|
|
336
|
+
rho: float, volvol: float, timegrid: np.ndarray, nb_path: int,
|
|
337
|
+
Z0: np.ndarray, Z1: np.ndarray):
|
|
338
|
+
h = timegrid[1] - timegrid[0]
|
|
339
|
+
# assert np.all(np.isclose(np.diff(timegrid)[1:], h)) and timegrid[0] == 0.0
|
|
340
|
+
# assert Z0.shape == (timegrid.size - 1, nb_path) and Z1.shape == (timegrid.size - 1, nb_path)
|
|
341
|
+
|
|
342
|
+
y0 = np.zeros((1, nb_path))
|
|
343
|
+
|
|
344
|
+
vol_h = v_init.copy()
|
|
345
|
+
y_h = np.zeros((1, nb_path))
|
|
346
|
+
log_spot_h = np.ones((1, nb_path)) * log_s0
|
|
347
|
+
|
|
348
|
+
for idx, _ in enumerate(timegrid[:-1]):
|
|
349
|
+
vol_h, y_h, log_spot_h = log_spot_full_solve2_f64(nodes, weight, v0, y0, theta, kappa1, kappa2,
|
|
350
|
+
log_spot_h, vol_h, y_h, rho, volvol, h, nb_path,
|
|
351
|
+
Z0[idx], Z1[idx])
|
|
352
|
+
|
|
353
|
+
return log_spot_h, vol_h, y_h
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@njit(cache=False, fastmath=True)
|
|
357
|
+
def diffus_sde_solve_f32(y0: np.ndarray, weight: np.ndarray, weight_sum: np.ndarray,
|
|
358
|
+
volvol_weight_sum: np.ndarray, h: float, sqrt_h: float, nb_path: int,
|
|
359
|
+
z_rand: np.ndarray):
|
|
360
|
+
assert y0.shape == weight.shape and y0.shape[-1] == nb_path
|
|
361
|
+
assert z_rand.shape == (nb_path,)
|
|
362
|
+
yw = np.sum(weight * y0, axis=0)
|
|
363
|
+
|
|
364
|
+
half = np.float32(0.5)
|
|
365
|
+
one = np.float32(1.0)
|
|
366
|
+
dW = z_rand * sqrt_h
|
|
367
|
+
Yh = yw * np.exp(-half * volvol_weight_sum * volvol_weight_sum * h + volvol_weight_sum * dW)
|
|
368
|
+
|
|
369
|
+
Q = one / weight_sum * (Yh - yw)
|
|
370
|
+
Yh_vec = y0.copy()
|
|
371
|
+
for i in range(Yh_vec.shape[0]):
|
|
372
|
+
Yh_vec[i] += Q
|
|
373
|
+
|
|
374
|
+
return Yh_vec
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@njit(cache=False, fastmath=True)
|
|
378
|
+
def drift_diffus_strand_f32(nodes: np.ndarray, v0: np.ndarray, theta: float, kappa1: float, kappa2: float,
|
|
379
|
+
volvol_weight_sum: np.ndarray, v_init: np.ndarray, weight: np.ndarray, h: float,
|
|
380
|
+
sqrt_h: float, weight_sum: np.ndarray, nb_path: int, z_rand: np.ndarray):
|
|
381
|
+
half = np.float32(0.5)
|
|
382
|
+
D_inn = drift_ode_solve2_f32(nodes, v0, theta, kappa1, kappa2, v_init, weight, half * h)
|
|
383
|
+
S_inn = diffus_sde_solve_f32(D_inn, weight, weight_sum, volvol_weight_sum, h, sqrt_h, nb_path, z_rand)
|
|
384
|
+
sol = drift_ode_solve2_f32(nodes, v0, theta, kappa1, kappa2, S_inn, weight, half * h)
|
|
385
|
+
|
|
386
|
+
return sol
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@njit(cache=False, fastmath=True)
|
|
390
|
+
def log_spot_full_solve2_f32(nodes: np.ndarray, weight: np.ndarray, wlam: np.ndarray, w_inv: np.ndarray,
|
|
391
|
+
w_lam_v0: np.ndarray, weight_sum: np.ndarray, v0: np.ndarray, y0: np.ndarray,
|
|
392
|
+
theta: float, kappa1: float, kappa2: float, log_s: np.ndarray, v: np.ndarray, y: np.ndarray,
|
|
393
|
+
rho: float, rho_comp: float, inv_volvol: float, volvol_weight_sum: np.ndarray,
|
|
394
|
+
h: float, sqrt_h: float, nb_path: int, z0: np.ndarray, z1: np.ndarray):
|
|
395
|
+
assert nodes.shape == weight.shape and weight.ndim == 2
|
|
396
|
+
assert v.shape == weight.shape and v.shape[-1] == nb_path
|
|
397
|
+
assert y.shape == (1, nb_path)
|
|
398
|
+
assert log_s.shape == (1, nb_path)
|
|
399
|
+
assert v0.shape == weight.shape and v0.shape[-1] == nb_path
|
|
400
|
+
assert y0.shape == (1, nb_path)
|
|
401
|
+
assert z0.shape == (nb_path,) and z1.shape == (nb_path,)
|
|
402
|
+
|
|
403
|
+
half = np.float32(0.5)
|
|
404
|
+
eps = np.float32(1e-6)
|
|
405
|
+
|
|
406
|
+
vol_h = drift_diffus_strand_f32(nodes, v0, theta, kappa1, kappa2, volvol_weight_sum, v, weight, h, sqrt_h, weight_sum, nb_path, z0)
|
|
407
|
+
volw_h = np.sum(weight * vol_h, axis=0)
|
|
408
|
+
idx_bad = np.nonzero(np.logical_or(np.isnan(volw_h), volw_h <= 0.0))[0]
|
|
409
|
+
vol_h[:, idx_bad] = eps
|
|
410
|
+
|
|
411
|
+
vw = np.sum(weight * v, axis=0)
|
|
412
|
+
|
|
413
|
+
c1 = half
|
|
414
|
+
c2 = half
|
|
415
|
+
|
|
416
|
+
sq_vw = np.square(vw)
|
|
417
|
+
sq_vhw = np.square(volw_h)
|
|
418
|
+
|
|
419
|
+
w_lam_vol = np.sum(wlam * v, axis=0)
|
|
420
|
+
w_lam_vol_h = np.sum(wlam * vol_h, axis=0)
|
|
421
|
+
|
|
422
|
+
term1 = inv_volvol * (((volw_h - vw) / h + c1 * w_lam_vol + c2 * w_lam_vol_h - w_lam_v0) * w_inv
|
|
423
|
+
- kappa1 * theta + (kappa1 - kappa2 * theta) * (c1 * vw + c2 * volw_h)
|
|
424
|
+
+ kappa2 * (c1 * sq_vw + c2 * sq_vhw)) * h
|
|
425
|
+
|
|
426
|
+
term2 = c1 * h * sq_vw + c2 * h * sq_vhw
|
|
427
|
+
log_spot_h = log_s - half * term2 + rho * term1 + rho_comp * np.sqrt(term2) * z1
|
|
428
|
+
|
|
429
|
+
y_h = y + half * h * (vw * vw + volw_h * volw_h)
|
|
430
|
+
|
|
431
|
+
return vol_h, y_h, log_spot_h
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
@njit(cache=False, fastmath=True)
|
|
435
|
+
def log_spot_full_combined_f32(nodes: np.ndarray, weight: np.ndarray,
|
|
436
|
+
v0: np.ndarray,
|
|
437
|
+
theta: float, kappa1: float, kappa2: float, log_s0: float, v_init: np.ndarray,
|
|
438
|
+
rho: float, volvol: float, timegrid: np.ndarray, nb_path: int,
|
|
439
|
+
Z0: np.ndarray, Z1: np.ndarray):
|
|
440
|
+
one = np.float32(1.0)
|
|
441
|
+
h = timegrid[1] - timegrid[0]
|
|
442
|
+
sqrt_h = np.sqrt(h)
|
|
443
|
+
wlam = weight * nodes
|
|
444
|
+
weight_sum = np.sum(weight, axis=0)
|
|
445
|
+
w_inv = one / weight_sum
|
|
446
|
+
w_lam_v0 = np.sum(wlam * v0, axis=0)
|
|
447
|
+
rho_comp = np.sqrt(one - rho * rho)
|
|
448
|
+
inv_volvol = one / volvol
|
|
449
|
+
volvol_weight_sum = volvol * weight_sum
|
|
450
|
+
|
|
451
|
+
y0 = np.zeros_like(v0[:1, :])
|
|
452
|
+
|
|
453
|
+
vol_h = v_init.copy()
|
|
454
|
+
y_h = np.zeros_like(y0)
|
|
455
|
+
log_spot_h = np.empty_like(y0)
|
|
456
|
+
log_spot_h[:] = log_s0
|
|
457
|
+
|
|
458
|
+
for idx, _ in enumerate(timegrid[:-1]):
|
|
459
|
+
vol_h, y_h, log_spot_h = log_spot_full_solve2_f32(nodes, weight, wlam, w_inv, w_lam_v0, weight_sum, v0, y0,
|
|
460
|
+
theta, kappa1, kappa2, log_spot_h, vol_h, y_h, rho,
|
|
461
|
+
rho_comp, inv_volvol, volvol_weight_sum, h, sqrt_h,
|
|
462
|
+
nb_path, Z0[idx], Z1[idx])
|
|
463
|
+
|
|
464
|
+
return log_spot_h, vol_h, y_h
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def log_spot_full_combined(nodes: np.ndarray, weight: np.ndarray,
|
|
468
|
+
v0: np.ndarray,
|
|
469
|
+
theta: float, kappa1: float, kappa2: float, log_s0: float, v_init: np.ndarray,
|
|
470
|
+
rho: float, volvol: float, timegrid: np.ndarray, nb_path: int,
|
|
471
|
+
Z0: np.ndarray, Z1: np.ndarray):
|
|
472
|
+
"""
|
|
473
|
+
Dispatch to a float64-stable kernel by default; use float32 kernel only when all inputs are float32.
|
|
474
|
+
"""
|
|
475
|
+
if (nodes.dtype == np.float32 and weight.dtype == np.float32 and v0.dtype == np.float32 and
|
|
476
|
+
timegrid.dtype == np.float32 and Z0.dtype == np.float32 and Z1.dtype == np.float32):
|
|
477
|
+
return log_spot_full_combined_f32(nodes, weight, v0, theta, kappa1, kappa2, log_s0, v_init,
|
|
478
|
+
rho, volvol, timegrid, nb_path, Z0, Z1)
|
|
479
|
+
return log_spot_full_combined_f64(nodes, weight, v0, theta, kappa1, kappa2, log_s0, v_init,
|
|
480
|
+
rho, volvol, timegrid, nb_path, Z0, Z1)
|
|
481
|
+
|
|
482
|
+
|