stochvolmodels 1.1.3__tar.gz → 1.1.5__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.3/stochvolmodels.egg-info → stochvolmodels-1.1.5}/PKG-INFO +1 -1
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/pyproject.toml +1 -1
- stochvolmodels-1.1.5/stochvolmodels/examples/run_hawkes_pricer.py +50 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/examples/run_lognormal_sv_pricer.py +24 -17
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/hawkes_jd_pricer.py +1 -1
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/logsv/logsv_params.py +8 -8
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/logsv_pricer.py +38 -16
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/model_pricer.py +5 -1
- stochvolmodels-1.1.5/stochvolmodels/pricers/rough_logsv/expm.py +452 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/rough_logsv/split_simulation.py +92 -8
- stochvolmodels-1.1.5/stochvolmodels/pricers/rough_logsv/test_kernel_approx.py +15 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5/stochvolmodels.egg-info}/PKG-INFO +1 -1
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels.egg-info/SOURCES.txt +2 -0
- stochvolmodels-1.1.3/stochvolmodels/pricers/rough_logsv/test_kernel_approx.py +0 -16
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/.gitignore +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/LICENSE.txt +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/MANIFEST.in +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/README.md +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/forward_var/calibrate_forward_var.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/il_hedging/README.md +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/il_hedging/logsv_figures.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/il_hedging/run_logsv_for_il_payoff.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/inverse_options/README.md +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/inverse_options/compare_net_delta.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/logsv_model_wtih_quadratic_drift/README.md +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/logsv_model_wtih_quadratic_drift/article_figures.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/logsv_model_wtih_quadratic_drift/calibrations.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/logsv_model_wtih_quadratic_drift/compare_admis_reg.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/logsv_model_wtih_quadratic_drift/model_fit_to_options_timeseries.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/logsv_model_wtih_quadratic_drift/moments_vol_qvar.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/logsv_model_wtih_quadratic_drift/ode_sol_in_time.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/logsv_model_wtih_quadratic_drift/steady_state_pdf.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/logsv_model_wtih_quadratic_drift/vol_drift.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/risk_premia_gmm/check_kernel.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/risk_premia_gmm/gmm_slides.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/risk_premia_gmm/plot_gmm.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/risk_premia_gmm/q_kernel.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/risk_premia_gmm/run_gmm_fit.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/sv_for_factor_hjm/README.md +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/sv_for_factor_hjm/calibration_fig_5_6_7.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/sv_for_factor_hjm/calibration_fig_8_9.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/t_distribution/illustrations.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/t_distribution/market_data_fit.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/t_distribution/mc_pricer_with_kernel.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/volatility_models/README.md +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/volatility_models/article_figures.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/volatility_models/autocorr_fit.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/volatility_models/load_data.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/volatility_models/ss_distribution_fit.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/volatility_models/vol_beta.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/requirements.txt +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/setup.cfg +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/__init__.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/data/__init__.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/data/fetch_option_chain.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/data/option_chain.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/data/test_option_chain.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/examples/quick_run_lognormal_sv_pricer.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/examples/run_heston.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/examples/run_heston_sv_pricer.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/examples/run_pricing_options_on_qvar.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/__init__.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/analytic/__init__.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/analytic/bachelier.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/analytic/bsm.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/analytic/tdist.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/factor_hjm/double_exp_pricer.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/factor_hjm/factor_hjm_pricer.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/factor_hjm/rate_affine_expansion.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/factor_hjm/rate_core.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/factor_hjm/rate_evaluate.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/factor_hjm/rate_factor_basis.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/factor_hjm/rate_logsv_ivols.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/factor_hjm/rate_logsv_params.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/factor_hjm/rate_logsv_pricer.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/gmm_pricer.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/heston_pricer.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/logsv/__init__.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/logsv/affine_expansion.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/logsv/vol_moments_ode.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/rough_logsv/RoughKernel.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/tdist_pricer.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/tests/__init__.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/tests/bsm_mgf_pricer.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/tests/qv_pricer.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/utils/__init__.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/utils/config.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/utils/funcs.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/utils/mc_payoffs.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/utils/mgf_pricer.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/utils/plots.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/utils/var_swap_pricer.py +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels.egg-info/dependency_links.txt +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels.egg-info/requires.txt +0 -0
- {stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/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.5
|
|
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>
|
|
@@ -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.5"
|
|
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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
|
|
2
|
+
# built in
|
|
3
|
+
import numpy as np
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
from scipy.optimize import minimize
|
|
6
|
+
from numba.typed import List
|
|
7
|
+
from typing import Tuple, Optional, Dict, Any
|
|
8
|
+
from dataclasses import dataclass, asdict
|
|
9
|
+
from scipy.integrate import solve_ivp
|
|
10
|
+
from scipy.integrate._ivp.ivp import OdeResult
|
|
11
|
+
from enum import Enum
|
|
12
|
+
|
|
13
|
+
# stochvolmodels pricers
|
|
14
|
+
import stochvolmodels.utils.mgf_pricer as mgfp
|
|
15
|
+
from stochvolmodels.utils.config import VariableType
|
|
16
|
+
from stochvolmodels.utils.mc_payoffs import compute_mc_vars_payoff
|
|
17
|
+
from stochvolmodels.pricers.model_pricer import ModelPricer, ModelParams
|
|
18
|
+
from stochvolmodels.utils.funcs import to_flat_np_array, set_time_grid, timer, set_seed
|
|
19
|
+
|
|
20
|
+
# data
|
|
21
|
+
from stochvolmodels.data.option_chain import OptionChain
|
|
22
|
+
from stochvolmodels import OptionChain, HawkesJDPricer, HawkesJDParams
|
|
23
|
+
|
|
24
|
+
pricer = HawkesJDPricer()
|
|
25
|
+
|
|
26
|
+
params = HawkesJDParams(sigma=0.1,
|
|
27
|
+
shift_p=0.25, # positive jump threshold
|
|
28
|
+
mean_p=0.00,
|
|
29
|
+
shift_m=-0.25,
|
|
30
|
+
mean_m=-0.00,
|
|
31
|
+
lambda_p=1.0,
|
|
32
|
+
theta_p=0.01,
|
|
33
|
+
kappa_p=300.0,
|
|
34
|
+
beta1_p=0.0,
|
|
35
|
+
beta2_p=0.0,
|
|
36
|
+
lambda_m=1.0,
|
|
37
|
+
theta_m=0.01,
|
|
38
|
+
kappa_m=300.0,
|
|
39
|
+
beta1_m=0.0,
|
|
40
|
+
beta2_m=0.0)
|
|
41
|
+
|
|
42
|
+
option_chain = OptionChain.get_uniform_chain(ttms=np.array([1.0/12.0]),
|
|
43
|
+
ids=np.array(['1m']),
|
|
44
|
+
forwards=np.array([100.0]),
|
|
45
|
+
strikes=100.0*np.linspace(0.5, 1.5, 30))
|
|
46
|
+
|
|
47
|
+
pricer.plot_model_ivols(option_chain=option_chain,
|
|
48
|
+
params=params)
|
|
49
|
+
|
|
50
|
+
plt.show()
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/examples/run_lognormal_sv_pricer.py
RENAMED
|
@@ -11,7 +11,7 @@ from enum import Enum
|
|
|
11
11
|
import stochvolmodels as sv
|
|
12
12
|
from stochvolmodels.utils import plots as plot
|
|
13
13
|
from stochvolmodels import LogSVPricer, LogSvParams, OptionChain, LogsvModelCalibrationType
|
|
14
|
-
|
|
14
|
+
# from uuid import uuid4
|
|
15
15
|
|
|
16
16
|
class LocalTests(Enum):
|
|
17
17
|
COMPUTE_MODEL_PRICES = 1
|
|
@@ -142,9 +142,9 @@ def run_local_test(local_test: LocalTests):
|
|
|
142
142
|
nb_path=10000,
|
|
143
143
|
nb_steps_per_year=360,
|
|
144
144
|
seed=10)
|
|
145
|
-
params0 = LogSvParams(sigma0=0.
|
|
146
|
-
params0.H = 0.
|
|
147
|
-
params0.approximate_kernel(T=btc_option_chain.ttms[-1]
|
|
145
|
+
params0 = LogSvParams(sigma0=0.377, theta=0.347, kappa1=1.29, kappa2=1.93, beta=2.45, volvol=1.81)
|
|
146
|
+
params0.H = 0.1
|
|
147
|
+
params0.approximate_kernel(T=btc_option_chain.ttms[-1])
|
|
148
148
|
|
|
149
149
|
option_prices_ttm, option_std_ttm = sv.rough_logsv_mc_chain_pricer_fixed_randoms(ttms=btc_option_chain.ttms,
|
|
150
150
|
forwards=btc_option_chain.forwards,
|
|
@@ -167,16 +167,15 @@ def run_local_test(local_test: LocalTests):
|
|
|
167
167
|
elif local_test == LocalTests.BENCHM_ROUGH_PRICER:
|
|
168
168
|
btc_option_chain = sv.get_btc_test_chain_data()
|
|
169
169
|
# params0 = LogSvParams(sigma0=0.8, theta=1.0, kappa1=2.21, kappa2=1.0, beta=0.15, volvol=1.0)
|
|
170
|
-
params0 = LogSvParams(sigma0=
|
|
171
|
-
nb_path =
|
|
172
|
-
H = 0.
|
|
173
|
-
N = 2
|
|
170
|
+
params0 = LogSvParams(sigma0=0.377, theta=0.347, kappa1=1.29, kappa2=1.93, beta=2.45, volvol=1.81)
|
|
171
|
+
nb_path = 10000
|
|
172
|
+
H = 0.1
|
|
174
173
|
seed = 1
|
|
175
174
|
|
|
176
175
|
def rough_vol():
|
|
177
176
|
params1 = LogSvParams.copy(params0)
|
|
178
177
|
params1.H = H
|
|
179
|
-
params1.approximate_kernel(T=btc_option_chain.ttms[-1]
|
|
178
|
+
params1.approximate_kernel(T=btc_option_chain.ttms[-1])
|
|
180
179
|
|
|
181
180
|
Z0, Z1, grid_ttms = sv.get_randoms_for_rough_vol_chain_valuation(ttms=btc_option_chain.ttms,
|
|
182
181
|
nb_path=nb_path,
|
|
@@ -250,7 +249,7 @@ def run_local_test(local_test: LocalTests):
|
|
|
250
249
|
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda z, _: '{:.0%}'.format(z)))
|
|
251
250
|
ax.xaxis.set_major_formatter(mticker.FuncFormatter(lambda z, _: '{:.2f}'.format(z)))
|
|
252
251
|
ax.legend()
|
|
253
|
-
fig.suptitle(f"Conventional LogSV model vs Rough LogSV, H={H:.2f} via
|
|
252
|
+
fig.suptitle(f"Conventional LogSV model vs Rough LogSV, H={H:.2f} via Markovian approximation\n"
|
|
254
253
|
f"{params0.to_str()}",
|
|
255
254
|
color = "darkblue", fontsize = 14)
|
|
256
255
|
|
|
@@ -269,23 +268,31 @@ def run_local_test(local_test: LocalTests):
|
|
|
269
268
|
|
|
270
269
|
elif local_test == LocalTests.CALIBRATE_MODEL_TO_BTC_OPTIONS_WITH_MC:
|
|
271
270
|
btc_option_chain = sv.get_btc_test_chain_data()
|
|
272
|
-
params0 = LogSvParams(sigma0=0.8, theta=1.0, kappa1=2.21, kappa2=
|
|
273
|
-
params0.H = 0.
|
|
274
|
-
params0.approximate_kernel(T=btc_option_chain.ttms[-1]
|
|
271
|
+
params0 = LogSvParams(sigma0=0.8, theta=1.0, kappa1=2.21, kappa2=2.18, beta=0.15, volvol=2.0)
|
|
272
|
+
params0.H = 0.2
|
|
273
|
+
params0.approximate_kernel(T=btc_option_chain.ttms[-1])
|
|
275
274
|
btc_calibrated_params = logsv_pricer.calibrate_model_params_to_chain(option_chain=btc_option_chain,
|
|
276
275
|
params0=params0,
|
|
277
276
|
model_calibration_type=LogsvModelCalibrationType.PARAMS4,
|
|
278
277
|
constraints_type=sv.ConstraintsType.INVERSE_MARTINGALE,
|
|
279
278
|
calibration_engine=sv.CalibrationEngine.ROUGH_MC,
|
|
280
|
-
nb_path=
|
|
279
|
+
nb_path=5000,
|
|
281
280
|
seed=7)
|
|
282
281
|
print(btc_calibrated_params)
|
|
283
|
-
logsv_pricer.plot_model_ivols_vs_bid_ask(option_chain=btc_option_chain,
|
|
284
|
-
params=btc_calibrated_params
|
|
282
|
+
fig = logsv_pricer.plot_model_ivols_vs_bid_ask(option_chain=btc_option_chain,
|
|
283
|
+
params=btc_calibrated_params,
|
|
284
|
+
mode='mc',
|
|
285
|
+
use_rough_mc=True,
|
|
286
|
+
seed=16,
|
|
287
|
+
nb_steps=360
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# uuid = str(uuid4())
|
|
291
|
+
# plot.save_fig(fig=fig, local_path=r"c:/temp/", file_name=f"fit_quality_{uuid}")
|
|
285
292
|
|
|
286
293
|
plt.show()
|
|
287
294
|
|
|
288
295
|
|
|
289
296
|
if __name__ == '__main__':
|
|
290
297
|
|
|
291
|
-
run_local_test(local_test=LocalTests.
|
|
298
|
+
run_local_test(local_test=LocalTests.ROUGH_MC_WITH_FIXED_RANDOMS)
|
|
@@ -33,16 +33,16 @@ class LogSvParams(ModelParams):
|
|
|
33
33
|
self.kappa2 = self.kappa1 / self.theta
|
|
34
34
|
assert 1e-4 < self.H <= 0.5
|
|
35
35
|
|
|
36
|
-
def approximate_kernel(self, T: float
|
|
37
|
-
|
|
38
|
-
if self.H >= 0.4:
|
|
39
|
-
N = N if N<=2 else 2
|
|
40
|
-
self.nodes, self.weights = european_rule(self.H, N, T)
|
|
41
|
-
elif N > 1 and self.H<0.49:
|
|
42
|
-
self.nodes, self.weights = european_rule(self.H, N, T)
|
|
43
|
-
else:
|
|
36
|
+
def approximate_kernel(self, T: float):
|
|
37
|
+
if 0.49 < self.H <= 0.5:
|
|
44
38
|
self.weights = np.array([1.0])
|
|
45
39
|
self.nodes = np.array([1e-3])
|
|
40
|
+
return
|
|
41
|
+
elif 0.4 < self.H <= 0.49:
|
|
42
|
+
N = 2
|
|
43
|
+
else:
|
|
44
|
+
N = 3
|
|
45
|
+
self.nodes, self.weights = european_rule(self.H, N, T)
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def to_dict(self) -> Dict[str, Any]:
|
|
@@ -87,22 +87,44 @@ class LogSVPricer(ModelPricer):
|
|
|
87
87
|
**kwargs
|
|
88
88
|
) -> (List[np.ndarray], List[np.ndarray]):
|
|
89
89
|
vol_backbone_etas = params.get_vol_backbone_etas(ttms=option_chain.ttms)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
90
|
+
if 'use_rough_mc' in kwargs and kwargs['use_rough_mc']:
|
|
91
|
+
assert 'seed' in kwargs
|
|
92
|
+
seed = kwargs['seed']
|
|
93
|
+
Z0, Z1, grid_ttms = get_randoms_for_rough_vol_chain_valuation(ttms=option_chain.ttms, nb_path=nb_path,
|
|
94
|
+
nb_steps_per_year=nb_steps, seed=seed)
|
|
95
|
+
return rough_logsv_mc_chain_pricer_fixed_randoms(ttms=option_chain.ttms,
|
|
96
|
+
forwards=option_chain.forwards,
|
|
97
|
+
discfactors=option_chain.discfactors,
|
|
98
|
+
strikes_ttms=option_chain.strikes_ttms,
|
|
99
|
+
optiontypes_ttms=option_chain.optiontypes_ttms,
|
|
100
|
+
Z0=Z0,
|
|
101
|
+
Z1=Z1,
|
|
102
|
+
sigma0=params.sigma0,
|
|
103
|
+
theta=params.theta,
|
|
104
|
+
kappa1=params.kappa1,
|
|
105
|
+
kappa2=params.kappa2,
|
|
106
|
+
beta=params.beta,
|
|
107
|
+
orthog_vol=params.volvol,
|
|
108
|
+
weights=params.weights,
|
|
109
|
+
nodes=params.nodes,
|
|
110
|
+
timegrids=grid_ttms)
|
|
111
|
+
else:
|
|
112
|
+
return logsv_mc_chain_pricer(v0=params.sigma0,
|
|
113
|
+
theta=params.theta,
|
|
114
|
+
kappa1=params.kappa1,
|
|
115
|
+
kappa2=params.kappa2,
|
|
116
|
+
beta=params.beta,
|
|
117
|
+
volvol=params.volvol,
|
|
118
|
+
vol_backbone_etas=vol_backbone_etas,
|
|
119
|
+
ttms=option_chain.ttms,
|
|
120
|
+
forwards=option_chain.forwards,
|
|
121
|
+
discfactors=option_chain.discfactors,
|
|
122
|
+
strikes_ttms=option_chain.strikes_ttms,
|
|
123
|
+
optiontypes_ttms=option_chain.optiontypes_ttms,
|
|
124
|
+
is_spot_measure=is_spot_measure,
|
|
125
|
+
variable_type=variable_type,
|
|
126
|
+
nb_path=nb_path,
|
|
127
|
+
nb_steps_per_year=nb_steps or int(360 * np.max(option_chain.ttms)) + 1)
|
|
106
128
|
|
|
107
129
|
def set_vol_scaler(self, option_chain: OptionChain) -> float:
|
|
108
130
|
"""
|
|
@@ -331,7 +331,11 @@ class ModelPricer(ABC):
|
|
|
331
331
|
plot model slice_t vols
|
|
332
332
|
optimized for 2*2 figure
|
|
333
333
|
"""
|
|
334
|
-
|
|
334
|
+
if 'mode' in kwargs and kwargs['mode'] == 'mc':
|
|
335
|
+
model_ivols = self.compute_mc_chain_implied_vols(option_chain=option_chain, params=params, **kwargs)[3]
|
|
336
|
+
else:
|
|
337
|
+
model_ivols = self.compute_model_ivols_for_chain(option_chain=option_chain, params=params, **kwargs)
|
|
338
|
+
|
|
335
339
|
|
|
336
340
|
num_slices = len(option_chain.ttms)
|
|
337
341
|
with sns.axes_style('darkgrid'):
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import math
|
|
3
|
+
import cmath
|
|
4
|
+
from numba import njit, prange
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# ---------- small complex helpers (Numba-friendly) ----------
|
|
8
|
+
|
|
9
|
+
@njit(cache=True)
|
|
10
|
+
def _cabs(z):
|
|
11
|
+
return math.hypot(z.real, z.imag)
|
|
12
|
+
|
|
13
|
+
@njit(cache=True)
|
|
14
|
+
def _csqrt(z):
|
|
15
|
+
# principal square root
|
|
16
|
+
r = _cabs(z)
|
|
17
|
+
if r == 0.0:
|
|
18
|
+
return 0.0 + 0.0j
|
|
19
|
+
# sqrt in polar form
|
|
20
|
+
theta = math.atan2(z.imag, z.real)
|
|
21
|
+
sr = math.sqrt(r)
|
|
22
|
+
ht = 0.5 * theta
|
|
23
|
+
return complex(sr * math.cos(ht), sr * math.sin(ht))
|
|
24
|
+
|
|
25
|
+
@njit(cache=True)
|
|
26
|
+
def _ccbrt(z):
|
|
27
|
+
# principal cube root
|
|
28
|
+
r = _cabs(z)
|
|
29
|
+
if r == 0.0:
|
|
30
|
+
return 0.0 + 0.0j
|
|
31
|
+
theta = math.atan2(z.imag, z.real)
|
|
32
|
+
cr = r ** (1.0 / 3.0)
|
|
33
|
+
tt = theta / 3.0
|
|
34
|
+
return complex(cr * math.cos(tt), cr * math.sin(tt))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------- Step 1: cubic roots for p(r)=0 ----------
|
|
38
|
+
|
|
39
|
+
@njit(cache=True)
|
|
40
|
+
def _cubic_roots_monic(a2, a1, a0):
|
|
41
|
+
"""
|
|
42
|
+
Solve r^3 + a2*r^2 + a1*r + a0 = 0 (monic cubic).
|
|
43
|
+
Returns 3 complex roots (principal-branch Cardano).
|
|
44
|
+
"""
|
|
45
|
+
# Depress: r = y - a2/3
|
|
46
|
+
one_third = 1.0 / 3.0
|
|
47
|
+
shift = a2 * one_third
|
|
48
|
+
|
|
49
|
+
p = a1 - (a2 * a2) * one_third
|
|
50
|
+
q = (2.0 * a2 * a2 * a2) / 27.0 - (a2 * a1) / 3.0 + a0
|
|
51
|
+
|
|
52
|
+
# Discriminant: Δ = (q/2)^2 + (p/3)^3
|
|
53
|
+
q2 = 0.5 * q
|
|
54
|
+
p3 = p * one_third
|
|
55
|
+
Delta = q2 * q2 + p3 * p3 * p3 # complex-safe
|
|
56
|
+
|
|
57
|
+
sqrtD = _csqrt(Delta)
|
|
58
|
+
u = _ccbrt(-q2 + sqrtD)
|
|
59
|
+
v = _ccbrt(-q2 - sqrtD)
|
|
60
|
+
|
|
61
|
+
y1 = u + v
|
|
62
|
+
|
|
63
|
+
# cube roots of unity:
|
|
64
|
+
# y2 = -(u+v)/2 + i*sqrt(3)/2*(u-v)
|
|
65
|
+
# y3 = -(u+v)/2 - i*sqrt(3)/2*(u-v)
|
|
66
|
+
half = 0.5
|
|
67
|
+
s3_2 = 0.5 * math.sqrt(3.0)
|
|
68
|
+
|
|
69
|
+
uv = u - v
|
|
70
|
+
y2 = -half * y1 + complex(-s3_2 * uv.imag, s3_2 * uv.real) # i*s3_2*(u-v)
|
|
71
|
+
y3 = -half * y1 - complex(-s3_2 * uv.imag, s3_2 * uv.real)
|
|
72
|
+
|
|
73
|
+
r1 = y1 - shift
|
|
74
|
+
r2 = y2 - shift
|
|
75
|
+
r3 = y3 - shift
|
|
76
|
+
return r1, r2, r3
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------- Step 2: solve Vandermonde for c0,c1,c2 ----------
|
|
80
|
+
|
|
81
|
+
@njit(cache=True)
|
|
82
|
+
def _solve_3x3_complex(M, b):
|
|
83
|
+
"""
|
|
84
|
+
Solve M x = b for 3x3 complex M with partial pivoting.
|
|
85
|
+
Returns x (complex length-3).
|
|
86
|
+
"""
|
|
87
|
+
A = M.copy()
|
|
88
|
+
rhs = b.copy()
|
|
89
|
+
|
|
90
|
+
# Forward elimination
|
|
91
|
+
for k in range(3):
|
|
92
|
+
# pivot
|
|
93
|
+
piv = k
|
|
94
|
+
best = _cabs(A[k, k])
|
|
95
|
+
for i in range(k + 1, 3):
|
|
96
|
+
v = _cabs(A[i, k])
|
|
97
|
+
if v > best:
|
|
98
|
+
best = v
|
|
99
|
+
piv = i
|
|
100
|
+
if piv != k:
|
|
101
|
+
# swap rows
|
|
102
|
+
for j in range(3):
|
|
103
|
+
tmp = A[k, j]
|
|
104
|
+
A[k, j] = A[piv, j]
|
|
105
|
+
A[piv, j] = tmp
|
|
106
|
+
tmp = rhs[k]
|
|
107
|
+
rhs[k] = rhs[piv]
|
|
108
|
+
rhs[piv] = tmp
|
|
109
|
+
|
|
110
|
+
akk = A[k, k]
|
|
111
|
+
# If akk is (near) zero, this can blow up; caller should avoid repeated roots.
|
|
112
|
+
for i in range(k + 1, 3):
|
|
113
|
+
factor = A[i, k] / akk
|
|
114
|
+
A[i, k] = 0.0 + 0.0j
|
|
115
|
+
for j in range(k + 1, 3):
|
|
116
|
+
A[i, j] = A[i, j] - factor * A[k, j]
|
|
117
|
+
rhs[i] = rhs[i] - factor * rhs[k]
|
|
118
|
+
|
|
119
|
+
# Back substitution
|
|
120
|
+
x = np.empty(3, dtype=np.complex128)
|
|
121
|
+
for i in range(2, -1, -1):
|
|
122
|
+
s = rhs[i]
|
|
123
|
+
for j in range(i + 1, 3):
|
|
124
|
+
s = s - A[i, j] * x[j]
|
|
125
|
+
x[i] = s / A[i, i]
|
|
126
|
+
return x
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ---------- Step 3: build e^A = c0 I + c1 A + c2 A^2 ----------
|
|
130
|
+
|
|
131
|
+
@njit(cache=True)
|
|
132
|
+
def expA_n3_numba(kappa, lambdas, w, tol=1e-10):
|
|
133
|
+
"""
|
|
134
|
+
Computes exp(A) for A = -kappa * 1*w^T - diag(lambdas), where 1=(1,1,1)^T.
|
|
135
|
+
Steps:
|
|
136
|
+
1) build cubic p(r)=det(rI-A) and find roots r1,r2,r3
|
|
137
|
+
2) solve Vandermonde for c0,c1,c2 so exp(A)=c0 I + c1 A + c2 A^2
|
|
138
|
+
3) evaluate polynomial in A
|
|
139
|
+
|
|
140
|
+
Returns: 3x3 float64 matrix.
|
|
141
|
+
"""
|
|
142
|
+
l1, l2, l3 = lambdas[0], lambdas[1], lambdas[2]
|
|
143
|
+
w1, w2, w3 = w[0], w[1], w[2]
|
|
144
|
+
|
|
145
|
+
# --- Build monic cubic coefficients for p(r)=0:
|
|
146
|
+
# p(r) = (r+λ1)(r+λ2)(r+λ3) + κ[ w1(r+λ2)(r+λ3) + w2(r+λ1)(r+λ3) + w3(r+λ1)(r+λ2) ]
|
|
147
|
+
s1 = l1 + l2 + l3
|
|
148
|
+
s2 = l1*l2 + l1*l3 + l2*l3
|
|
149
|
+
s3 = l1*l2*l3
|
|
150
|
+
|
|
151
|
+
W = w1 + w2 + w3
|
|
152
|
+
B = w1*(l2+l3) + w2*(l1+l3) + w3*(l1+l2)
|
|
153
|
+
C = w1*(l2*l3) + w2*(l1*l3) + w3*(l1*l2)
|
|
154
|
+
|
|
155
|
+
a2 = s1 + kappa * W
|
|
156
|
+
a1 = s2 + kappa * B
|
|
157
|
+
a0 = s3 + kappa * C
|
|
158
|
+
|
|
159
|
+
r1, r2, r3 = _cubic_roots_monic(a2 + 0.0j, a1 + 0.0j, a0 + 0.0j)
|
|
160
|
+
|
|
161
|
+
# Guard against repeated/near-repeated roots (Vandermonde gets unstable)
|
|
162
|
+
if _cabs(r1 - r2) < tol or _cabs(r1 - r3) < tol or _cabs(r2 - r3) < tol:
|
|
163
|
+
# Simple fallback: scaling-and-squaring with truncated Taylor (kept Numba-friendly)
|
|
164
|
+
# This is not "steps 1-2-3" anymore, but prevents crashes when roots collide.
|
|
165
|
+
A = np.zeros((3, 3), dtype=np.float64)
|
|
166
|
+
for i in range(3):
|
|
167
|
+
for j in range(3):
|
|
168
|
+
A[i, j] = -kappa * w[j]
|
|
169
|
+
A[i, i] -= lambdas[i]
|
|
170
|
+
|
|
171
|
+
# scale
|
|
172
|
+
# crude bound on ||A||_1
|
|
173
|
+
colsum_max = 0.0
|
|
174
|
+
for j in range(3):
|
|
175
|
+
s = 0.0
|
|
176
|
+
for i in range(3):
|
|
177
|
+
s += abs(A[i, j])
|
|
178
|
+
if s > colsum_max:
|
|
179
|
+
colsum_max = s
|
|
180
|
+
s_pow = 0
|
|
181
|
+
if colsum_max > 0.5:
|
|
182
|
+
s_pow = int(math.ceil(math.log(colsum_max / 0.5) / math.log(2.0)))
|
|
183
|
+
|
|
184
|
+
As = A / (2.0 ** s_pow)
|
|
185
|
+
|
|
186
|
+
# exp(As) via Taylor up to N terms
|
|
187
|
+
E = np.eye(3, dtype=np.float64)
|
|
188
|
+
term = np.eye(3, dtype=np.float64)
|
|
189
|
+
N = 30
|
|
190
|
+
for n in range(1, N + 1):
|
|
191
|
+
# term = term @ As / n
|
|
192
|
+
newterm = np.zeros((3, 3), dtype=np.float64)
|
|
193
|
+
for i in range(3):
|
|
194
|
+
for j in range(3):
|
|
195
|
+
acc = 0.0
|
|
196
|
+
for k in range(3):
|
|
197
|
+
acc += term[i, k] * As[k, j]
|
|
198
|
+
newterm[i, j] = acc / n
|
|
199
|
+
term = newterm
|
|
200
|
+
E += term
|
|
201
|
+
|
|
202
|
+
# square back
|
|
203
|
+
for _ in range(s_pow):
|
|
204
|
+
EE = np.zeros((3, 3), dtype=np.float64)
|
|
205
|
+
for i in range(3):
|
|
206
|
+
for j in range(3):
|
|
207
|
+
acc = 0.0
|
|
208
|
+
for k in range(3):
|
|
209
|
+
acc += E[i, k] * E[k, j]
|
|
210
|
+
EE[i, j] = acc
|
|
211
|
+
E = EE
|
|
212
|
+
return E
|
|
213
|
+
|
|
214
|
+
# Vandermonde: [1 r r^2][c0,c1,c2]^T = exp(r)
|
|
215
|
+
V = np.empty((3, 3), dtype=np.complex128)
|
|
216
|
+
b = np.empty(3, dtype=np.complex128)
|
|
217
|
+
|
|
218
|
+
roots = (r1, r2, r3)
|
|
219
|
+
for i in range(3):
|
|
220
|
+
r = roots[i]
|
|
221
|
+
V[i, 0] = 1.0 + 0.0j
|
|
222
|
+
V[i, 1] = r
|
|
223
|
+
V[i, 2] = r * r
|
|
224
|
+
b[i] = np.exp(r)
|
|
225
|
+
|
|
226
|
+
c = _solve_3x3_complex(V, b)
|
|
227
|
+
c0, c1, c2 = c[0], c[1], c[2]
|
|
228
|
+
|
|
229
|
+
# Build A (real)
|
|
230
|
+
A = np.zeros((3, 3), dtype=np.float64)
|
|
231
|
+
for i in range(3):
|
|
232
|
+
for j in range(3):
|
|
233
|
+
A[i, j] = -kappa * w[j]
|
|
234
|
+
A[i, i] -= lambdas[i]
|
|
235
|
+
|
|
236
|
+
# A^2
|
|
237
|
+
A2 = np.zeros((3, 3), dtype=np.float64)
|
|
238
|
+
for i in range(3):
|
|
239
|
+
for j in range(3):
|
|
240
|
+
acc = 0.0
|
|
241
|
+
for k in range(3):
|
|
242
|
+
acc += A[i, k] * A[k, j]
|
|
243
|
+
A2[i, j] = acc
|
|
244
|
+
|
|
245
|
+
# E = c0 I + c1 A + c2 A^2 (complex -> take real part)
|
|
246
|
+
E = np.zeros((3, 3), dtype=np.float64)
|
|
247
|
+
for i in range(3):
|
|
248
|
+
for j in range(3):
|
|
249
|
+
val = c1 * (A[i, j] + 0.0j) + c2 * (A2[i, j] + 0.0j)
|
|
250
|
+
if i == j:
|
|
251
|
+
val += c0
|
|
252
|
+
E[i, j] = val.real # imaginary parts should be ~0 for real A
|
|
253
|
+
return E
|
|
254
|
+
|
|
255
|
+
@njit(cache=True)
|
|
256
|
+
def expA_n1_numba (kappa, lambdas, w):
|
|
257
|
+
"""
|
|
258
|
+
n=1: A = (lambda1 + kappa*w1). Returns scalar
|
|
259
|
+
lambdas: shape (1,)
|
|
260
|
+
w: shape (1,)
|
|
261
|
+
"""
|
|
262
|
+
a = -(lambdas[0] + kappa * w[0])
|
|
263
|
+
out = math.exp(a)
|
|
264
|
+
return out
|
|
265
|
+
|
|
266
|
+
@njit(cache=True)
|
|
267
|
+
def _sinhc(z, tol=1e-12):
|
|
268
|
+
"""sinh(z)/z with small-z series for stability (Numba-friendly)."""
|
|
269
|
+
if abs(z) < tol:
|
|
270
|
+
z2 = z * z
|
|
271
|
+
return 1.0 + 22 / 6.0 + (z2 *z2) / 120.0
|
|
272
|
+
return cmath.sinh(z) / z
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@njit(cache=True)
|
|
276
|
+
def expA_n2_numba(kappa, lambdas, w):
|
|
277
|
+
"""n=2: A = -kappa*1*w^T - diag(lambdas). Returns 2x2 exp(A).
|
|
278
|
+
lambdas: shape (2,)
|
|
279
|
+
w: shape (2,)
|
|
280
|
+
"""
|
|
281
|
+
l1, l2 = lambdas[0], lambdas [1]
|
|
282
|
+
w1, w2 = w[0], w[1]
|
|
283
|
+
|
|
284
|
+
# A entries
|
|
285
|
+
a11 = -l1 - kappa * w1
|
|
286
|
+
a12 = -kappa * w2
|
|
287
|
+
a21 = -kappa * w1
|
|
288
|
+
a22 = -l2 - kappa * w2
|
|
289
|
+
|
|
290
|
+
tr = a11 + a22
|
|
291
|
+
det = a11 * a22 - a12 * a21
|
|
292
|
+
|
|
293
|
+
half_tr = 0.5 * tr
|
|
294
|
+
Delta = cmath.sqrt((half_tr * half_tr) - det + 0.0j)
|
|
295
|
+
|
|
296
|
+
ehalf = math.exp(half_tr) # half_tr is real
|
|
297
|
+
c = cmath.cosh(Delta)
|
|
298
|
+
s_over = _sinhc(Delta)
|
|
299
|
+
|
|
300
|
+
# B = A - (tr/2) I
|
|
301
|
+
b11 = a11 - half_tr
|
|
302
|
+
b22 = a22 - half_tr
|
|
303
|
+
b12 = a12
|
|
304
|
+
b21 = a21
|
|
305
|
+
|
|
306
|
+
# exp(A) = e^{tr/2} [ c I + (sinh(Delta)/Delta) * B ]
|
|
307
|
+
E11 = ehalf * (c + s_over * (b11 + 0.0j))
|
|
308
|
+
E22 = ehalf * (c + s_over * (b22 + 0.0j))
|
|
309
|
+
E12 = ehalf * (s_over * (b12 + 0.0j))
|
|
310
|
+
E21 = ehalf * (s_over * (b21 + 0.0j))
|
|
311
|
+
|
|
312
|
+
out = np.empty((2, 2), dtype = np.float64)
|
|
313
|
+
out[0, 0] = E11.real
|
|
314
|
+
out[0, 1] = E12.real
|
|
315
|
+
out[1, 0] = E21.real
|
|
316
|
+
out[1, 1] = E22.real
|
|
317
|
+
return out
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@njit(parallel=True, cache=True)
|
|
322
|
+
def batch_expA_n3(kappas: np.ndarray, lambdas_all: np.ndarray, w_all: np.ndarray):
|
|
323
|
+
"""
|
|
324
|
+
kappas: shape (N,)
|
|
325
|
+
lambdas_all: shape (N,3)
|
|
326
|
+
w_all: shape (N,3)
|
|
327
|
+
returns: shape (N,3,3)
|
|
328
|
+
"""
|
|
329
|
+
N = kappas.shape[-1]
|
|
330
|
+
out = np.empty((N,3,3), dtype=np.float64)
|
|
331
|
+
for i in prange(N):
|
|
332
|
+
out[i] = expA_n3_numba(kappas[i], lambdas_all[i], w_all[i])
|
|
333
|
+
return out
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@njit(parallel=True, cache=True)
|
|
337
|
+
def batch_expA_n2(kappas: np.ndarray, lambdas_all: np.ndarray, w_all: np.ndarray):
|
|
338
|
+
"""
|
|
339
|
+
kappas: shape (N,)
|
|
340
|
+
lambdas_all: shape (N,2)
|
|
341
|
+
w_all: shape (N,2)
|
|
342
|
+
returns: shape (N,2,2)
|
|
343
|
+
"""
|
|
344
|
+
N = kappas.shape[-1]
|
|
345
|
+
out = np.empty((N,2,2), dtype=np.float64)
|
|
346
|
+
for i in prange(N):
|
|
347
|
+
out[i] = expA_n2_numba(kappas[i], lambdas_all[i], w_all[i])
|
|
348
|
+
return out
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@njit(parallel=True, cache=True)
|
|
352
|
+
def batch_expA_n1(kappas: np.ndarray, lambdas_all: np.ndarray, w_all: np.ndarray):
|
|
353
|
+
"""
|
|
354
|
+
kappas: shape (N,)
|
|
355
|
+
lambdas_all: shape (N,1)
|
|
356
|
+
w_all: shape (N,1)
|
|
357
|
+
returns: shape (N,1,1)
|
|
358
|
+
"""
|
|
359
|
+
N = kappas.shape[-1]
|
|
360
|
+
out = np.empty((N,1,1), dtype=np.float64)
|
|
361
|
+
for i in prange(N):
|
|
362
|
+
out[i,0,0] = expA_n1_numba(kappas[i], lambdas_all[i], w_all[i])
|
|
363
|
+
return out
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
@njit(parallel=True, cache=True)
|
|
368
|
+
def batch_expA(kappas: np.ndarray, lambdas_all: np.ndarray, w_all: np.ndarray):
|
|
369
|
+
"""
|
|
370
|
+
kappas: shape (N,)
|
|
371
|
+
lambdas_all: shape (N,d)
|
|
372
|
+
w_all: shape (N,d)
|
|
373
|
+
returns: shape (N,d,d)
|
|
374
|
+
"""
|
|
375
|
+
d = lambdas_all.shape[-1]
|
|
376
|
+
if d==1:
|
|
377
|
+
return batch_expA_n1(kappas, lambdas_all, w_all)
|
|
378
|
+
elif d==2:
|
|
379
|
+
return batch_expA_n2(kappas, lambdas_all, w_all)
|
|
380
|
+
elif d==3:
|
|
381
|
+
return batch_expA_n3(kappas, lambdas_all, w_all)
|
|
382
|
+
else:
|
|
383
|
+
raise ValueError("Only d=1,2,3 are supported")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
#########################################################################
|
|
387
|
+
@njit(cache=True)
|
|
388
|
+
def invA_rank1_numba_general(kappa, lambdas, w, tol=1e-14):
|
|
389
|
+
"""Analytic inverse for A = -kappa*1*w^T - diag(lambdas), any n>=1.
|
|
390
|
+
|
|
391
|
+
Inputs:
|
|
392
|
+
kappa: float
|
|
393
|
+
lambdas: (n,) float64, diagonal entries of L (must be nonzero)
|
|
394
|
+
w: (n.) float64
|
|
395
|
+
tol: singularity tolerance for denom
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Ainv: (n,n) float64. If singular/ill-conditioned, returns all-Nan matrix.
|
|
399
|
+
"""
|
|
400
|
+
n = lambdas.shape[0]
|
|
401
|
+
Ainv = np.empty((n, n), dtype=np.float64)
|
|
402
|
+
|
|
403
|
+
# d_i = 1/lambda_i, z_i = w_i/lambda_i, s = sum z_i
|
|
404
|
+
d = np.empty(n, dtype=np.float64)
|
|
405
|
+
z = np.empty(n, dtype=np.float64)
|
|
406
|
+
|
|
407
|
+
s = 0.0
|
|
408
|
+
for i in range(n):
|
|
409
|
+
lam = lambdas[i]
|
|
410
|
+
if lam == 0.0:
|
|
411
|
+
# singular because L^{-1} doesn't exist
|
|
412
|
+
for r in range(n):
|
|
413
|
+
for c in range(n):
|
|
414
|
+
Ainv[r, c] = np.nan
|
|
415
|
+
return Ainv
|
|
416
|
+
di = 1.0 / lam
|
|
417
|
+
d[i] = di
|
|
418
|
+
zi = w[i] * di
|
|
419
|
+
z[i] = zi
|
|
420
|
+
s += zi
|
|
421
|
+
|
|
422
|
+
denom = 1.0 + kappa * s
|
|
423
|
+
if abs(denom) < tol:
|
|
424
|
+
for r in range(n):
|
|
425
|
+
for c in range(n):
|
|
426
|
+
Ainv[r, c] = np.nan
|
|
427
|
+
return Ainv
|
|
428
|
+
|
|
429
|
+
alpha = kappa / denom
|
|
430
|
+
for i in range(n):
|
|
431
|
+
di = d[i]
|
|
432
|
+
for j in range(n):
|
|
433
|
+
val = alpha * di * z[j]
|
|
434
|
+
if i == j:
|
|
435
|
+
val -= di
|
|
436
|
+
Ainv[i, j] = val
|
|
437
|
+
return Ainv
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@njit(parallel=True, cache=True)
|
|
441
|
+
def batch_invA(kappas: np.ndarray, lambdas_all: np.ndarray, w_all: np.ndarray):
|
|
442
|
+
"""
|
|
443
|
+
kappas: shape (N,)
|
|
444
|
+
lambdas_all: shape (N,3)
|
|
445
|
+
w_all: shape (N,3)
|
|
446
|
+
returns: shape (N,3,3)
|
|
447
|
+
"""
|
|
448
|
+
N, d = w_all.shape
|
|
449
|
+
out = np.empty((N,d,d), dtype=np.float64)
|
|
450
|
+
for i in prange(N):
|
|
451
|
+
out[i] = invA_rank1_numba_general(kappas[i], lambdas_all[i], w_all[i])
|
|
452
|
+
return out
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/rough_logsv/split_simulation.py
RENAMED
|
@@ -6,13 +6,14 @@ from scipy.linalg import expm
|
|
|
6
6
|
from numba import njit
|
|
7
7
|
from numba import njit, objmode
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
from stochvolmodels.pricers.rough_logsv.expm import batch_expA, batch_invA
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
# from stochvolmodels.utils.config import VariableType
|
|
13
13
|
# from RoughKernel import european_rule
|
|
14
14
|
# from stochvolmodels.pricers.logsv_pricer import LogSvParams
|
|
15
15
|
|
|
16
|
+
|
|
16
17
|
@njit(cache=False, fastmath=True)
|
|
17
18
|
def drift_ode_solve(nodes: np.ndarray, v0: np.ndarray, theta: float, kappa1: float, kappa2: float,
|
|
18
19
|
z0: np.ndarray, weight: np.ndarray, h: float):
|
|
@@ -22,7 +23,7 @@ def drift_ode_solve(nodes: np.ndarray, v0: np.ndarray, theta: float, kappa1: flo
|
|
|
22
23
|
----------
|
|
23
24
|
nodes : (fixed argument) exponents x_i, array of size (n,)
|
|
24
25
|
v0 : (fixed argument) array of size (n, nb_path)
|
|
25
|
-
theta : long-run level,
|
|
26
|
+
theta : long-run level, scalar
|
|
26
27
|
kappa1 : linear mean-reversion speed, scalar
|
|
27
28
|
kappa2 : quadratic mean-reversion speed, scalar
|
|
28
29
|
z0 : initial values, array of size (n, nb_path)
|
|
@@ -34,16 +35,32 @@ def drift_ode_solve(nodes: np.ndarray, v0: np.ndarray, theta: float, kappa1: flo
|
|
|
34
35
|
Array of size (n, nb_path)
|
|
35
36
|
"""
|
|
36
37
|
assert nodes.shape == v0.shape == z0.shape == weight.shape
|
|
37
|
-
n =
|
|
38
|
+
n = z0.shape[0]
|
|
38
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
|
|
39
54
|
|
|
40
55
|
# s1 = -nodes[:, None] * (z0 - v0) * h + (kappa1 + kappa2 * z0w) * (theta - z0w) * h
|
|
41
56
|
# s2 = -nodes[:, None] * (z0 + 0.5 * s1 - v0) * h + (kappa1 + kappa2 * (z0w + 0.5 * s1)) * (theta - (z0w + 0.5 * s1)) * h
|
|
42
57
|
# Dzh = z0 + s2
|
|
43
58
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
47
64
|
|
|
48
65
|
# lamda_vec = np.repeat(lamda, n)
|
|
49
66
|
# theta_vec = np.repeat(theta, n)[:, None]
|
|
@@ -57,6 +74,73 @@ def drift_ode_solve(nodes: np.ndarray, v0: np.ndarray, theta: float, kappa1: flo
|
|
|
57
74
|
|
|
58
75
|
return Dzh
|
|
59
76
|
|
|
77
|
+
|
|
78
|
+
@njit(cache=False, fastmath=True)
|
|
79
|
+
def drift_ode_solve3(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
|
+
z0w = np.sum(weight * z0, axis=0)
|
|
101
|
+
kappa = kappa1 + kappa2 * z0w
|
|
102
|
+
|
|
103
|
+
b_ = np.zeros((nb_path, n))
|
|
104
|
+
for k in range(n):
|
|
105
|
+
b_[:, k] = kappa * theta + nodes[k] * v0[k]
|
|
106
|
+
|
|
107
|
+
eAh = batch_expA(kappa, nodes.T * h, weight.T * h)
|
|
108
|
+
I = np.identity(n)
|
|
109
|
+
invA = batch_invA(kappa, nodes.T, weight.T)
|
|
110
|
+
tmp2 = np.zeros((nb_path, n, n))
|
|
111
|
+
for i in range(n):
|
|
112
|
+
for j in range(n):
|
|
113
|
+
for k in range(n):
|
|
114
|
+
tmp2[:,i,j] += invA[:,i,k] * (eAh[:, k, j] - I[k,j])
|
|
115
|
+
|
|
116
|
+
Dzh_v1 = np.zeros((nb_path, n))
|
|
117
|
+
for i in range(n):
|
|
118
|
+
for j in range(n):
|
|
119
|
+
Dzh_v1[:, i] += eAh[:, i, j] * z0[j, :] + tmp2[:, i, j] * b_[:, j]
|
|
120
|
+
# for p in range(nb_path):
|
|
121
|
+
# Dzh_v1[p] = eAh[p] @ z0[..., p] + tmp2[p] @ b_[p]
|
|
122
|
+
|
|
123
|
+
# def ff2():
|
|
124
|
+
# lamda = kappa1 + kappa2 * z0w
|
|
125
|
+
# coeff0_vec = np.repeat(kappa1 * theta, n)
|
|
126
|
+
# Dzh_v2 = np.zeros_like(z0)
|
|
127
|
+
# for p in range(nb_path):
|
|
128
|
+
# diag_x = np.diag(nodes[:, p])
|
|
129
|
+
# lamda_vec = np.repeat(lamda[p], n)
|
|
130
|
+
# A = -np.outer(lamda_vec, weight[:, p]) - diag_x # matrix of size (n,n)
|
|
131
|
+
# b = coeff0_vec + diag_x @ v0[:, p]
|
|
132
|
+
# eAh_v2 = expm(A * h)
|
|
133
|
+
# Dzh_v2[:, p] = eAh_v2 @ z0[:, p] + (np.linalg.inv(A) @ (eAh_v2 - I)) @ b
|
|
134
|
+
#
|
|
135
|
+
# return Dzh_v2
|
|
136
|
+
#
|
|
137
|
+
# Dzh_v2 = ff2()
|
|
138
|
+
# diff = np.linalg.norm(Dzh_v2-Dzh_v1.T)
|
|
139
|
+
# assert diff < 1e-12
|
|
140
|
+
|
|
141
|
+
return Dzh_v1.T
|
|
142
|
+
|
|
143
|
+
|
|
60
144
|
@njit(cache=False, fastmath=True)
|
|
61
145
|
def diffus_sde_solve(y0: np.ndarray, weight: np.ndarray, volvol: float, h: float, nb_path: int,
|
|
62
146
|
z_rand: np.ndarray):
|
|
@@ -101,9 +185,9 @@ def drift_diffus_strand(nodes: np.ndarray, v0: np.ndarray, theta: float, kappa1:
|
|
|
101
185
|
-------
|
|
102
186
|
|
|
103
187
|
"""
|
|
104
|
-
D_inn =
|
|
188
|
+
D_inn = drift_ode_solve3(nodes, v0, theta, kappa1, kappa2, v_init, weight, 0.5 * h)
|
|
105
189
|
S_inn = diffus_sde_solve(D_inn, weight, volvol, h, nb_path, z_rand)
|
|
106
|
-
sol =
|
|
190
|
+
sol = drift_ode_solve3(nodes, v0, theta, kappa1, kappa2, S_inn, weight, 0.5 * h)
|
|
107
191
|
|
|
108
192
|
return sol
|
|
109
193
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from stochvolmodels import LogSvParams
|
|
3
|
+
|
|
4
|
+
if __name__ == "__main__":
|
|
5
|
+
H = 0.4464700054758044
|
|
6
|
+
N = 2
|
|
7
|
+
T = 1.
|
|
8
|
+
|
|
9
|
+
params0 = LogSvParams(sigma0 = 1.32, theta=0.47, kappa1=4.0, kappa2=2.0, beta=0.45, volvol=0.83)
|
|
10
|
+
for H in np.linspace(0.3, 0.5, 150):
|
|
11
|
+
params0.H = H
|
|
12
|
+
print(f" *** H={H} *** ")
|
|
13
|
+
params0.approximate_kernel(T=T)
|
|
14
|
+
print(params0.weights)
|
|
15
|
+
print(params0.nodes)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stochvolmodels
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.5
|
|
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>
|
|
@@ -47,6 +47,7 @@ stochvolmodels/data/fetch_option_chain.py
|
|
|
47
47
|
stochvolmodels/data/option_chain.py
|
|
48
48
|
stochvolmodels/data/test_option_chain.py
|
|
49
49
|
stochvolmodels/examples/quick_run_lognormal_sv_pricer.py
|
|
50
|
+
stochvolmodels/examples/run_hawkes_pricer.py
|
|
50
51
|
stochvolmodels/examples/run_heston.py
|
|
51
52
|
stochvolmodels/examples/run_heston_sv_pricer.py
|
|
52
53
|
stochvolmodels/examples/run_lognormal_sv_pricer.py
|
|
@@ -76,6 +77,7 @@ stochvolmodels/pricers/logsv/affine_expansion.py
|
|
|
76
77
|
stochvolmodels/pricers/logsv/logsv_params.py
|
|
77
78
|
stochvolmodels/pricers/logsv/vol_moments_ode.py
|
|
78
79
|
stochvolmodels/pricers/rough_logsv/RoughKernel.py
|
|
80
|
+
stochvolmodels/pricers/rough_logsv/expm.py
|
|
79
81
|
stochvolmodels/pricers/rough_logsv/split_simulation.py
|
|
80
82
|
stochvolmodels/pricers/rough_logsv/test_kernel_approx.py
|
|
81
83
|
stochvolmodels/tests/__init__.py
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
from stochvolmodels import LogSvParams
|
|
3
|
-
|
|
4
|
-
if __name__ == "__main__":
|
|
5
|
-
H = 0.4464700054758044
|
|
6
|
-
N = 2
|
|
7
|
-
T = 1.
|
|
8
|
-
|
|
9
|
-
params0 = LogSvParams(sigma0 = 1.32, theta=0.47, kappa1=4.0, kappa2=2.0, beta=0.45, volvol=0.83)
|
|
10
|
-
for N in [1, 2, 3]:
|
|
11
|
-
for H in np.linspace(0.3, 0.49, 150):
|
|
12
|
-
params0.H = H
|
|
13
|
-
print(f" *** N={N}, H={H} *** ")
|
|
14
|
-
params0.approximate_kernel(T=T, N=N)
|
|
15
|
-
print(params0.weights)
|
|
16
|
-
print(params0.nodes)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/forward_var/calibrate_forward_var.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/il_hedging/run_logsv_for_il_payoff.py
RENAMED
|
File without changes
|
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/inverse_options/compare_net_delta.py
RENAMED
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/logsv_model_wtih_quadratic_drift/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/sv_for_factor_hjm/calibration_fig_5_6_7.py
RENAMED
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/sv_for_factor_hjm/calibration_fig_8_9.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/t_distribution/mc_pricer_with_kernel.py
RENAMED
|
File without changes
|
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/volatility_models/article_figures.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/my_papers/volatility_models/ss_distribution_fit.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/examples/run_heston_sv_pricer.py
RENAMED
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/examples/run_pricing_options_on_qvar.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/factor_hjm/double_exp_pricer.py
RENAMED
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/factor_hjm/factor_hjm_pricer.py
RENAMED
|
File without changes
|
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/factor_hjm/rate_core.py
RENAMED
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/factor_hjm/rate_evaluate.py
RENAMED
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/factor_hjm/rate_factor_basis.py
RENAMED
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/factor_hjm/rate_logsv_ivols.py
RENAMED
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/factor_hjm/rate_logsv_params.py
RENAMED
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/factor_hjm/rate_logsv_pricer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/logsv/affine_expansion.py
RENAMED
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/logsv/vol_moments_ode.py
RENAMED
|
File without changes
|
{stochvolmodels-1.1.3 → stochvolmodels-1.1.5}/stochvolmodels/pricers/rough_logsv/RoughKernel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|