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.
Files changed (98) hide show
  1. {stochvolmodels-1.1.5/stochvolmodels.egg-info → stochvolmodels-1.1.7}/PKG-INFO +3 -2
  2. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/pyproject.toml +4 -3
  3. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/requirements.txt +0 -0
  4. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/logsv_pricer.py +10 -8
  5. stochvolmodels-1.1.7/stochvolmodels/pricers/rough_logsv/split_simulation.py +482 -0
  6. stochvolmodels-1.1.7/stochvolmodels/tests/rough_logsv_perf.py +302 -0
  7. stochvolmodels-1.1.7/stochvolmodels/tests/test_rough_logsv_pricer_regression/test_rough_logsv_pricer_pricing_regression.yml +50 -0
  8. stochvolmodels-1.1.7/stochvolmodels/tests/test_rough_logsv_pricer_regression.py +50 -0
  9. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7/stochvolmodels.egg-info}/PKG-INFO +3 -2
  10. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels.egg-info/SOURCES.txt +3 -0
  11. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels.egg-info/requires.txt +2 -1
  12. stochvolmodels-1.1.5/stochvolmodels/pricers/rough_logsv/split_simulation.py +0 -262
  13. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/.gitignore +0 -0
  14. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/LICENSE.txt +0 -0
  15. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/MANIFEST.in +0 -0
  16. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/README.md +0 -0
  17. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/forward_var/calibrate_forward_var.py +0 -0
  18. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/il_hedging/README.md +0 -0
  19. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/il_hedging/logsv_figures.py +0 -0
  20. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/il_hedging/run_logsv_for_il_payoff.py +0 -0
  21. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/inverse_options/README.md +0 -0
  22. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/inverse_options/compare_net_delta.py +0 -0
  23. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/logsv_model_wtih_quadratic_drift/README.md +0 -0
  24. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/logsv_model_wtih_quadratic_drift/article_figures.py +0 -0
  25. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/logsv_model_wtih_quadratic_drift/calibrations.py +0 -0
  26. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/logsv_model_wtih_quadratic_drift/compare_admis_reg.py +0 -0
  27. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/logsv_model_wtih_quadratic_drift/model_fit_to_options_timeseries.py +0 -0
  28. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/logsv_model_wtih_quadratic_drift/moments_vol_qvar.py +0 -0
  29. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/logsv_model_wtih_quadratic_drift/ode_sol_in_time.py +0 -0
  30. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/logsv_model_wtih_quadratic_drift/steady_state_pdf.py +0 -0
  31. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/logsv_model_wtih_quadratic_drift/vol_drift.py +0 -0
  32. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/risk_premia_gmm/check_kernel.py +0 -0
  33. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/risk_premia_gmm/gmm_slides.py +0 -0
  34. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/risk_premia_gmm/plot_gmm.py +0 -0
  35. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/risk_premia_gmm/q_kernel.py +0 -0
  36. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/risk_premia_gmm/run_gmm_fit.py +0 -0
  37. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/sv_for_factor_hjm/README.md +0 -0
  38. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/sv_for_factor_hjm/calibration_fig_5_6_7.py +0 -0
  39. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/sv_for_factor_hjm/calibration_fig_8_9.py +0 -0
  40. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/t_distribution/illustrations.py +0 -0
  41. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/t_distribution/market_data_fit.py +0 -0
  42. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/t_distribution/mc_pricer_with_kernel.py +0 -0
  43. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/volatility_models/README.md +0 -0
  44. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/volatility_models/article_figures.py +0 -0
  45. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/volatility_models/autocorr_fit.py +0 -0
  46. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/volatility_models/load_data.py +0 -0
  47. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/volatility_models/ss_distribution_fit.py +0 -0
  48. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/my_papers/volatility_models/vol_beta.py +0 -0
  49. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/setup.cfg +0 -0
  50. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/__init__.py +0 -0
  51. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/data/__init__.py +0 -0
  52. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/data/fetch_option_chain.py +0 -0
  53. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/data/option_chain.py +0 -0
  54. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/data/test_option_chain.py +0 -0
  55. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/examples/quick_run_lognormal_sv_pricer.py +0 -0
  56. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/examples/run_hawkes_pricer.py +0 -0
  57. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/examples/run_heston.py +0 -0
  58. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/examples/run_heston_sv_pricer.py +0 -0
  59. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/examples/run_lognormal_sv_pricer.py +0 -0
  60. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/examples/run_pricing_options_on_qvar.py +0 -0
  61. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/__init__.py +0 -0
  62. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/analytic/__init__.py +0 -0
  63. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/analytic/bachelier.py +0 -0
  64. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/analytic/bsm.py +0 -0
  65. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/analytic/tdist.py +0 -0
  66. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/factor_hjm/double_exp_pricer.py +0 -0
  67. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/factor_hjm/factor_hjm_pricer.py +0 -0
  68. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/factor_hjm/rate_affine_expansion.py +0 -0
  69. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/factor_hjm/rate_core.py +0 -0
  70. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/factor_hjm/rate_evaluate.py +0 -0
  71. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/factor_hjm/rate_factor_basis.py +0 -0
  72. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/factor_hjm/rate_logsv_ivols.py +0 -0
  73. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/factor_hjm/rate_logsv_params.py +0 -0
  74. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/factor_hjm/rate_logsv_pricer.py +0 -0
  75. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/gmm_pricer.py +0 -0
  76. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/hawkes_jd_pricer.py +0 -0
  77. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/heston_pricer.py +0 -0
  78. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/logsv/__init__.py +0 -0
  79. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/logsv/affine_expansion.py +0 -0
  80. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/logsv/logsv_params.py +0 -0
  81. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/logsv/vol_moments_ode.py +0 -0
  82. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/model_pricer.py +0 -0
  83. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/rough_logsv/RoughKernel.py +0 -0
  84. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/rough_logsv/expm.py +0 -0
  85. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/rough_logsv/test_kernel_approx.py +0 -0
  86. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/pricers/tdist_pricer.py +0 -0
  87. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/tests/__init__.py +0 -0
  88. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/tests/bsm_mgf_pricer.py +0 -0
  89. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/tests/qv_pricer.py +0 -0
  90. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/utils/__init__.py +0 -0
  91. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/utils/config.py +0 -0
  92. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/utils/funcs.py +0 -0
  93. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/utils/mc_payoffs.py +0 -0
  94. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/utils/mgf_pricer.py +0 -0
  95. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/utils/plots.py +0 -0
  96. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels/utils/var_swap_pricer.py +0 -0
  97. {stochvolmodels-1.1.5 → stochvolmodels-1.1.7}/stochvolmodels.egg-info/dependency_links.txt +0 -0
  98. {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.5
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>=7.0.0; extra == "dev"
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.5"
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>=7.0.0",
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
+ ]
@@ -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
- v0 = sigma0 / np.sum(weights) * np.ones((N,))
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
- print(f"Number of paths with negative vol: {np.sum(weights @ vol_str < 0.0)}")
910
- print(f"Mean spot Strand: {np.mean(np.exp(log_spot_str))}")
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
+