stochvolmodels 1.0.20__tar.gz → 1.0.22__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.0.20 → stochvolmodels-1.0.22}/PKG-INFO +2 -1
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/pyproject.toml +1 -1
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/__init__.py +1 -1
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/data/option_chain.py +23 -0
- stochvolmodels-1.0.22/stochvolmodels/examples/quick_run_lognormal_sv_pricer.py +34 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/examples/run_lognormal_sv_pricer.py +1 -3
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/heston_pricer.py +1 -1
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/logsv/affine_expansion.py +24 -16
- stochvolmodels-1.0.22/stochvolmodels/pricers/logsv/logsv_params.py +176 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/logsv/vol_moments_ode.py +49 -6
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/logsv_pricer.py +47 -159
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/model_pricer.py +3 -3
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/tests/qv_pricer.py +2 -1
- stochvolmodels-1.0.22/stochvolmodels/utils/var_swap_pricer.py +26 -0
- stochvolmodels-1.0.20/stochvolmodels/my_papers/il_hedging/README.md +0 -8
- stochvolmodels-1.0.20/stochvolmodels/my_papers/il_hedging/logsv_figures.py +0 -61
- stochvolmodels-1.0.20/stochvolmodels/my_papers/il_hedging/run_logsv_for_il_payoff.py +0 -150
- stochvolmodels-1.0.20/stochvolmodels/my_papers/inverse_options/README.md +0 -10
- stochvolmodels-1.0.20/stochvolmodels/my_papers/inverse_options/compare_net_delta.py +0 -168
- stochvolmodels-1.0.20/stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift/README.md +0 -13
- stochvolmodels-1.0.20/stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift/article_figures.py +0 -325
- stochvolmodels-1.0.20/stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift/calibrations.py +0 -190
- stochvolmodels-1.0.20/stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift/compare_admis_reg.py +0 -128
- stochvolmodels-1.0.20/stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift/model_fit_to_options_timeseries.py +0 -252
- stochvolmodels-1.0.20/stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift/moments_vol_qvar.py +0 -239
- stochvolmodels-1.0.20/stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift/ode_sol_in_time.py +0 -308
- stochvolmodels-1.0.20/stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift/steady_state_pdf.py +0 -251
- stochvolmodels-1.0.20/stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift/vol_drift.py +0 -82
- stochvolmodels-1.0.20/stochvolmodels/my_papers/risk_premia/check_kernel.py +0 -20
- stochvolmodels-1.0.20/stochvolmodels/my_papers/risk_premia/gmm_slides.py +0 -501
- stochvolmodels-1.0.20/stochvolmodels/my_papers/risk_premia/q_kernel.py +0 -53
- stochvolmodels-1.0.20/stochvolmodels/my_papers/t_distribution/__init__.py +0 -0
- stochvolmodels-1.0.20/stochvolmodels/my_papers/t_distribution/illustrations.py +0 -197
- stochvolmodels-1.0.20/stochvolmodels/my_papers/t_distribution/market_data_fit.py +0 -66
- stochvolmodels-1.0.20/stochvolmodels/my_papers/t_distribution/mc_pricer_with_kernel.py +0 -97
- stochvolmodels-1.0.20/stochvolmodels/my_papers/volatility_models/README.md +0 -10
- stochvolmodels-1.0.20/stochvolmodels/my_papers/volatility_models/__init__.py +0 -0
- stochvolmodels-1.0.20/stochvolmodels/my_papers/volatility_models/article_figures.py +0 -216
- stochvolmodels-1.0.20/stochvolmodels/my_papers/volatility_models/autocorr_fit.py +0 -235
- stochvolmodels-1.0.20/stochvolmodels/my_papers/volatility_models/load_data.py +0 -64
- stochvolmodels-1.0.20/stochvolmodels/my_papers/volatility_models/ss_distribution_fit.py +0 -327
- stochvolmodels-1.0.20/stochvolmodels/my_papers/volatility_models/vol_beta.py +0 -60
- stochvolmodels-1.0.20/stochvolmodels/pricers/__init__.py +0 -0
- stochvolmodels-1.0.20/stochvolmodels/pricers/analytic/__init__.py +0 -0
- stochvolmodels-1.0.20/stochvolmodels/pricers/logsv/__init__.py +0 -0
- stochvolmodels-1.0.20/stochvolmodels/tests/__init__.py +0 -0
- stochvolmodels-1.0.20/stochvolmodels/utils/__init__.py +0 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/LICENSE.txt +0 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/README.md +0 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/data/__init__.py +0 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/data/fetch_option_chain.py +0 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/data/test_option_chain.py +0 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/examples/run_gmm_fit.py +0 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/examples/run_heston.py +0 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/examples/run_heston_sv_pricer.py +0 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/examples/run_pricing_options_on_qvar.py +0 -0
- {stochvolmodels-1.0.20/stochvolmodels/my_papers → stochvolmodels-1.0.22/stochvolmodels/pricers}/__init__.py +0 -0
- {stochvolmodels-1.0.20/stochvolmodels/my_papers/il_hedging → stochvolmodels-1.0.22/stochvolmodels/pricers/analytic}/__init__.py +0 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/analytic/bachelier.py +0 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/analytic/bsm.py +0 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/analytic/tdist.py +0 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/gmm_pricer.py +0 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/hawkes_jd_pricer.py +0 -0
- {stochvolmodels-1.0.20/stochvolmodels/my_papers/inverse_options → stochvolmodels-1.0.22/stochvolmodels/pricers/logsv}/__init__.py +0 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/tdist_pricer.py +0 -0
- {stochvolmodels-1.0.20/stochvolmodels/my_papers/logsv_model_wtih_quadratic_drift → stochvolmodels-1.0.22/stochvolmodels/tests}/__init__.py +0 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/tests/bsm_mgf_pricer.py +0 -0
- {stochvolmodels-1.0.20/stochvolmodels/my_papers/risk_premia → stochvolmodels-1.0.22/stochvolmodels/utils}/__init__.py +0 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/utils/config.py +0 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/utils/funcs.py +0 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/utils/mc_payoffs.py +0 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/utils/mgf_pricer.py +0 -0
- {stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/utils/plots.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: stochvolmodels
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.22
|
|
4
4
|
Summary: Implementation of stochastic volatility models for option pricing
|
|
5
5
|
Home-page: https://github.com/ArturSepp/StochVolModels
|
|
6
6
|
License: LICENSE.txt
|
|
@@ -24,6 +24,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
24
24
|
Classifier: Programming Language :: Python :: 3.10
|
|
25
25
|
Classifier: Programming Language :: Python :: 3.11
|
|
26
26
|
Classifier: Programming Language :: Python :: 3.12
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
27
28
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
28
29
|
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
29
30
|
Requires-Dist: matplotlib (>=3.5.2)
|
|
@@ -104,10 +104,10 @@ from stochvolmodels.pricers.heston_pricer import (
|
|
|
104
104
|
from stochvolmodels.pricers.logsv_pricer import (
|
|
105
105
|
LOGSV_BTC_PARAMS,
|
|
106
106
|
LogSVPricer,
|
|
107
|
-
LogSvParams,
|
|
108
107
|
LogsvModelCalibrationType,
|
|
109
108
|
ConstraintsType
|
|
110
109
|
)
|
|
110
|
+
from stochvolmodels.pricers.logsv.logsv_params import LogSvParams
|
|
111
111
|
|
|
112
112
|
from stochvolmodels.pricers.gmm_pricer import (
|
|
113
113
|
GmmParams,
|
|
@@ -10,9 +10,12 @@ from __future__ import annotations
|
|
|
10
10
|
import numpy as np
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
from typing import Tuple, Optional
|
|
13
|
+
|
|
14
|
+
import pandas as pd
|
|
13
15
|
from numba.typed import List
|
|
14
16
|
|
|
15
17
|
import stochvolmodels.pricers.analytic.bsm as bsm
|
|
18
|
+
from stochvolmodels.utils.var_swap_pricer import compute_var_swap_strike
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
@dataclass
|
|
@@ -227,6 +230,26 @@ class OptionChain:
|
|
|
227
230
|
ask_prices=None if self.ask_prices is None else self.ask_prices[idx])
|
|
228
231
|
return option_slice
|
|
229
232
|
|
|
233
|
+
def get_slice_varswap_strikes(self, floor_with_atm_vols: bool = True) -> pd.Series:
|
|
234
|
+
varswap_strikes = np.zeros_like(self.ttms)
|
|
235
|
+
vols_ttms = self.get_mid_vols()
|
|
236
|
+
for idx, ttm in enumerate(self.ttms):
|
|
237
|
+
mid_prices = bsm.compute_bsm_vanilla_slice_prices(ttm=ttm,
|
|
238
|
+
forward=self.forwards[idx],
|
|
239
|
+
strikes=self.strikes_ttms[idx],
|
|
240
|
+
vols=vols_ttms[idx],
|
|
241
|
+
optiontypes=self.optiontypes_ttms[idx])
|
|
242
|
+
strikes = self.strikes_ttms[idx]
|
|
243
|
+
puts_cond = self.optiontypes_ttms[idx] == 'P'
|
|
244
|
+
puts = pd.Series(mid_prices[puts_cond], index=strikes[puts_cond])
|
|
245
|
+
calls = pd.Series(mid_prices[puts_cond == False], index=strikes[puts_cond == False])
|
|
246
|
+
varswap_strikes[idx] = compute_var_swap_strike(puts=puts, calls=calls, forward=self.forwards[idx], ttm=ttm)
|
|
247
|
+
|
|
248
|
+
if floor_with_atm_vols:
|
|
249
|
+
varswap_strikes = np.maximum(self.get_chain_atm_vols(), varswap_strikes)
|
|
250
|
+
|
|
251
|
+
return pd.Series(varswap_strikes, index=self.ttms)
|
|
252
|
+
|
|
230
253
|
@classmethod
|
|
231
254
|
def get_slices_as_chain(cls, option_chain: OptionChain, ids: List[str]) -> OptionChain:
|
|
232
255
|
"""
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
run few unit test to illustrate implementation of log-normal sv model analytics
|
|
3
|
+
"""
|
|
4
|
+
import numpy as np
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
from stochvolmodels import LogSVPricer, LogSvParams, LogsvModelCalibrationType, ConstraintsType, get_btc_test_chain_data
|
|
7
|
+
|
|
8
|
+
# 1. create instance of pricer
|
|
9
|
+
logsv_pricer = LogSVPricer()
|
|
10
|
+
|
|
11
|
+
# 2. define model params
|
|
12
|
+
params = LogSvParams(sigma0=1.0, theta=1.0, kappa1=5.0, kappa2=5.0, beta=0.2, volvol=2.0)
|
|
13
|
+
|
|
14
|
+
# 3. compute model prices for option slices
|
|
15
|
+
model_prices, vols = logsv_pricer.price_slice(params=params,
|
|
16
|
+
ttm=0.25,
|
|
17
|
+
forward=1.0,
|
|
18
|
+
strikes=np.array([0.8, 0.9, 1.0, 1.1]),
|
|
19
|
+
optiontypes=np.array(['P', 'P', 'C', 'C']))
|
|
20
|
+
print([f"{p:0.4f}, implied vol={v: 0.2%}" for p, v in zip(model_prices, vols)])
|
|
21
|
+
|
|
22
|
+
# 4. calibrate model to test option chain data
|
|
23
|
+
btc_option_chain = get_btc_test_chain_data()
|
|
24
|
+
params0 = LogSvParams(sigma0=1.0, theta=1.0, kappa1=2.21, kappa2=2.18, beta=0.15, volvol=2.0)
|
|
25
|
+
btc_calibrated_params = logsv_pricer.calibrate_model_params_to_chain(option_chain=btc_option_chain,
|
|
26
|
+
params0=params0,
|
|
27
|
+
model_calibration_type=LogsvModelCalibrationType.PARAMS4,
|
|
28
|
+
constraints_type=ConstraintsType.INVERSE_MARTINGALE)
|
|
29
|
+
print(btc_calibrated_params)
|
|
30
|
+
|
|
31
|
+
# 5. plot model implied vols
|
|
32
|
+
logsv_pricer.plot_model_ivols_vs_bid_ask(option_chain=btc_option_chain,
|
|
33
|
+
params=btc_calibrated_params)
|
|
34
|
+
plt.show()
|
{stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/examples/run_lognormal_sv_pricer.py
RENAMED
|
@@ -90,7 +90,6 @@ def run_unit_test(unit_test: UnitTests):
|
|
|
90
90
|
nb_path=100000)
|
|
91
91
|
|
|
92
92
|
elif unit_test == UnitTests.PLOT_FIT_TO_BITCOIN_OPTION_CHAIN:
|
|
93
|
-
|
|
94
93
|
btc_option_chain = sv.get_btc_test_chain_data()
|
|
95
94
|
btc_calibrated_params = LogSvParams(sigma0=0.8327, theta=1.0139, kappa1=4.8609, kappa2=4.7940, beta=0.1988, volvol=2.3694)
|
|
96
95
|
logsv_pricer.plot_model_ivols_vs_bid_ask(option_chain=btc_option_chain,
|
|
@@ -102,7 +101,6 @@ def run_unit_test(unit_test: UnitTests):
|
|
|
102
101
|
btc_calibrated_params = logsv_pricer.calibrate_model_params_to_chain(option_chain=btc_option_chain,
|
|
103
102
|
params0=params0,
|
|
104
103
|
model_calibration_type=LogsvModelCalibrationType.PARAMS4,
|
|
105
|
-
|
|
106
104
|
constraints_type=sv.ConstraintsType.INVERSE_MARTINGALE)
|
|
107
105
|
print(btc_calibrated_params)
|
|
108
106
|
logsv_pricer.plot_model_ivols_vs_bid_ask(option_chain=btc_option_chain,
|
|
@@ -112,7 +110,7 @@ def run_unit_test(unit_test: UnitTests):
|
|
|
112
110
|
|
|
113
111
|
if __name__ == '__main__':
|
|
114
112
|
|
|
115
|
-
unit_test = UnitTests.
|
|
113
|
+
unit_test = UnitTests.COMPUTE_MODEL_PRICES
|
|
116
114
|
|
|
117
115
|
is_run_all_tests = False
|
|
118
116
|
if is_run_all_tests:
|
|
@@ -412,7 +412,7 @@ def run_unit_test(unit_test: UnitTests):
|
|
|
412
412
|
|
|
413
413
|
elif unit_test == UnitTests.MC_COMPARISION_QVAR:
|
|
414
414
|
from stochvolmodels.pricers.logsv.vol_moments_ode import compute_analytic_qvar
|
|
415
|
-
from stochvolmodels
|
|
415
|
+
from stochvolmodels import LogSvParams
|
|
416
416
|
heston_pricer = HestonPricer()
|
|
417
417
|
ttms = {'1m': 1.0/12.0, '6m': 0.5}
|
|
418
418
|
option_chain = chains.get_qv_options_test_chain_data()
|
{stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/logsv/affine_expansion.py
RENAMED
|
@@ -36,7 +36,8 @@ def func_a_ode_quadratic_terms(theta: float,
|
|
|
36
36
|
phi: np.complex128,
|
|
37
37
|
psi: np.complex128,
|
|
38
38
|
is_spot_measure: bool = True,
|
|
39
|
-
expansion_order: ExpansionOrder = ExpansionOrder.FIRST
|
|
39
|
+
expansion_order: ExpansionOrder = ExpansionOrder.FIRST,
|
|
40
|
+
vol_backbone_eta: float = 1.0
|
|
40
41
|
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
41
42
|
"""
|
|
42
43
|
Matrices for the quadratic form A_t = A.T@M@A + L@A + H
|
|
@@ -45,14 +46,15 @@ def func_a_ode_quadratic_terms(theta: float,
|
|
|
45
46
|
vartheta2 = beta * beta + volvol * volvol
|
|
46
47
|
qv = theta * vartheta2
|
|
47
48
|
qv2 = theta2 * vartheta2
|
|
49
|
+
vol_backbone_eta2 = vol_backbone_eta * vol_backbone_eta
|
|
48
50
|
if is_spot_measure:
|
|
49
51
|
lamda = 0
|
|
50
52
|
kappa2_p = kappa2
|
|
51
53
|
kappa_p = kappa1 + kappa2 * theta
|
|
52
54
|
else:
|
|
53
|
-
lamda = beta*theta2
|
|
54
|
-
kappa2_p = kappa2-beta
|
|
55
|
-
kappa_p = kappa1 + kappa2 * theta - 2*beta*theta
|
|
55
|
+
lamda = beta*theta2*vol_backbone_eta
|
|
56
|
+
kappa2_p = kappa2-beta*vol_backbone_eta
|
|
57
|
+
kappa_p = kappa1 + kappa2 * theta - 2*beta*theta*vol_backbone_eta
|
|
56
58
|
|
|
57
59
|
# fill Ms: M should be of same type as L and H for numba, eventhough they are real
|
|
58
60
|
# utilize that M is symmetric
|
|
@@ -83,15 +85,15 @@ def func_a_ode_quadratic_terms(theta: float,
|
|
|
83
85
|
|
|
84
86
|
# fills Ls
|
|
85
87
|
L = np.zeros((n, n), dtype=np.complex128)
|
|
86
|
-
L[0, 1], L[0, 2] = lamda - theta2 * beta * phi, qv2
|
|
87
|
-
L[1, 1], L[1, 2] = -kappa_p - 2.0 * theta * beta * phi, 2.0 * (lamda + qv - theta2 * beta * phi)
|
|
88
|
-
L[2, 1], L[2, 2] = -kappa2_p - beta * phi, vartheta2 - 2.0 * kappa_p - 4.0 * theta * beta * phi
|
|
88
|
+
L[0, 1], L[0, 2] = lamda - theta2 * beta * vol_backbone_eta * phi, qv2
|
|
89
|
+
L[1, 1], L[1, 2] = -kappa_p - 2.0 * theta * beta * vol_backbone_eta * phi, 2.0 * (lamda + qv - theta2 * beta * vol_backbone_eta * phi)
|
|
90
|
+
L[2, 1], L[2, 2] = -kappa2_p - beta * vol_backbone_eta * phi, vartheta2 - 2.0 * kappa_p - 4.0 * theta * beta * vol_backbone_eta * phi
|
|
89
91
|
|
|
90
92
|
if expansion_order == ExpansionOrder.SECOND:
|
|
91
93
|
L[1, 3] = 3.0*qv2
|
|
92
|
-
L[2, 3], L[2, 4] = 3.0 * (2.0 * qv - theta2 * beta * phi), 6.0 * qv2
|
|
93
|
-
L[3, 2], L[3, 3], L[3, 4] = -2.0 * (kappa2_p + beta * phi), 3.0 * (vartheta2 - kappa_p - 2.0 * theta * beta * phi), 4.0 * (3.0 * qv - theta2 * beta * phi)
|
|
94
|
-
L[4, 3], L[4, 4] = -3.0 * (kappa2_p + beta * phi), 2.0 * (vartheta2 - 2.0 * kappa_p - 4.0 * theta * beta * phi)
|
|
94
|
+
L[2, 3], L[2, 4] = 3.0 * (2.0 * qv - theta2 * beta * vol_backbone_eta * phi), 6.0 * qv2
|
|
95
|
+
L[3, 2], L[3, 3], L[3, 4] = -2.0 * (kappa2_p + beta * vol_backbone_eta * phi), 3.0 * (vartheta2 - kappa_p - 2.0 * theta * beta * vol_backbone_eta * phi), 4.0 * (3.0 * qv - theta2 * beta * vol_backbone_eta * phi)
|
|
96
|
+
L[4, 3], L[4, 4] = -3.0 * (kappa2_p + beta * vol_backbone_eta * phi), 2.0 * (vartheta2 - 2.0 * kappa_p - 4.0 * theta * beta * vol_backbone_eta * phi)
|
|
95
97
|
|
|
96
98
|
# fill Hs
|
|
97
99
|
H = np.zeros(n, dtype=np.complex128)
|
|
@@ -99,7 +101,7 @@ def func_a_ode_quadratic_terms(theta: float,
|
|
|
99
101
|
rhs = (phi * (phi + 1.0) - 2.0 * psi)
|
|
100
102
|
else:
|
|
101
103
|
rhs = (phi * (phi - 1.0) - 2.0 * psi)
|
|
102
|
-
H[0], H[1], H[2] = 0.5 *
|
|
104
|
+
H[0], H[1], H[2] = 0.5*theta2 * vol_backbone_eta2 * rhs, theta * vol_backbone_eta2 * rhs, 0.5*vol_backbone_eta2 * rhs
|
|
103
105
|
|
|
104
106
|
return M, L, H
|
|
105
107
|
|
|
@@ -153,7 +155,8 @@ def solve_ode_for_a(ttm: float,
|
|
|
153
155
|
a_t0: Optional[np.ndarray] = None,
|
|
154
156
|
expansion_order: ExpansionOrder = ExpansionOrder.FIRST,
|
|
155
157
|
is_stiff_solver: bool = False,
|
|
156
|
-
dense_output: bool = False
|
|
158
|
+
dense_output: bool = False,
|
|
159
|
+
vol_backbone_eta: float = 1.0
|
|
157
160
|
) -> OdeResult:
|
|
158
161
|
"""
|
|
159
162
|
solve ode for given phi
|
|
@@ -167,7 +170,8 @@ def solve_ode_for_a(ttm: float,
|
|
|
167
170
|
phi=phi,
|
|
168
171
|
psi=psi,
|
|
169
172
|
expansion_order=expansion_order,
|
|
170
|
-
is_spot_measure=is_spot_measure
|
|
173
|
+
is_spot_measure=is_spot_measure,
|
|
174
|
+
vol_backbone_eta=vol_backbone_eta)
|
|
171
175
|
|
|
172
176
|
if a_t0 is None:
|
|
173
177
|
a_t0 = np.zeros_like(H, dtype=np.complex128)
|
|
@@ -361,7 +365,8 @@ def solve_a_ode_grid(phi_grid: np.ndarray,
|
|
|
361
365
|
is_spot_measure: bool = True,
|
|
362
366
|
a_t0: Optional[np.ndarray] = None,
|
|
363
367
|
is_stiff_solver: bool = False,
|
|
364
|
-
expansion_order: ExpansionOrder = ExpansionOrder.FIRST
|
|
368
|
+
expansion_order: ExpansionOrder = ExpansionOrder.FIRST,
|
|
369
|
+
vol_backbone_eta: float = 1.0
|
|
365
370
|
) -> np.ndarray:
|
|
366
371
|
"""
|
|
367
372
|
solve ode for range phi
|
|
@@ -382,7 +387,8 @@ def solve_a_ode_grid(phi_grid: np.ndarray,
|
|
|
382
387
|
is_stiff_solver=is_stiff_solver,
|
|
383
388
|
dense_output=False,
|
|
384
389
|
expansion_order=expansion_order,
|
|
385
|
-
is_spot_measure=is_spot_measure
|
|
390
|
+
is_spot_measure=is_spot_measure,
|
|
391
|
+
vol_backbone_eta=vol_backbone_eta)
|
|
386
392
|
|
|
387
393
|
a_t1 = np.zeros((phi_grid.shape[0], get_expansion_n(expansion_order)), dtype=np.complex128)
|
|
388
394
|
for idx, (phi, psi) in enumerate(zip(phi_grid, psi_grid)):
|
|
@@ -429,6 +435,7 @@ def compute_logsv_a_mgf_grid(ttm: float,
|
|
|
429
435
|
is_stiff_solver: bool = False,
|
|
430
436
|
is_analytic: bool = False,
|
|
431
437
|
is_spot_measure: bool = True,
|
|
438
|
+
vol_backbone_eta: float = 1.0,
|
|
432
439
|
**kwargs
|
|
433
440
|
) -> Tuple[np.ndarray, np.ndarray]:
|
|
434
441
|
"""
|
|
@@ -471,7 +478,8 @@ def compute_logsv_a_mgf_grid(ttm: float,
|
|
|
471
478
|
a_t0=a_t0,
|
|
472
479
|
is_stiff_solver=is_stiff_solver,
|
|
473
480
|
expansion_order=expansion_order,
|
|
474
|
-
is_spot_measure=is_spot_measure
|
|
481
|
+
is_spot_measure=is_spot_measure,
|
|
482
|
+
vol_backbone_eta=vol_backbone_eta)
|
|
475
483
|
|
|
476
484
|
y = sigma0 - theta
|
|
477
485
|
if expansion_order == ExpansionOrder.FIRST:
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from dataclasses import dataclass, asdict
|
|
2
|
+
from typing import Optional, Dict, Any
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from numpy import linalg as la
|
|
7
|
+
from qis import find_nearest
|
|
8
|
+
|
|
9
|
+
from stochvolmodels import VariableType
|
|
10
|
+
from stochvolmodels.pricers.model_pricer import ModelParams
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class LogSvParams(ModelParams):
|
|
15
|
+
"""
|
|
16
|
+
Implementation of model params class
|
|
17
|
+
"""
|
|
18
|
+
sigma0: float = 0.2
|
|
19
|
+
theta: float = 0.2
|
|
20
|
+
kappa1: float = 1.0
|
|
21
|
+
kappa2: Optional[float] = 2.5 # Optional is mapped to self.kappa1 / self.theta
|
|
22
|
+
beta: float = -1.0
|
|
23
|
+
volvol: float = 1.0
|
|
24
|
+
vol_backbone: pd.Series = None
|
|
25
|
+
|
|
26
|
+
def __post_init__(self):
|
|
27
|
+
if self.kappa2 is None:
|
|
28
|
+
self.kappa2 = self.kappa1 / self.theta
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
31
|
+
return asdict(self)
|
|
32
|
+
|
|
33
|
+
def to_str(self) -> str:
|
|
34
|
+
return f"sigma0={self.sigma0:0.2f}, theta={self.theta:0.2f}, kappa1={self.kappa1:0.2f}, kappa2={self.kappa2:0.2f}, " \
|
|
35
|
+
f"beta={self.beta:0.2f}, volvol={self.volvol:0.2f}"
|
|
36
|
+
|
|
37
|
+
def set_vol_backbone(self, vol_backbone: pd.Series) -> None:
|
|
38
|
+
self.vol_backbone = vol_backbone
|
|
39
|
+
|
|
40
|
+
def get_vol_backbone_eta(self, tau: float) -> float:
|
|
41
|
+
if self.vol_backbone is not None:
|
|
42
|
+
nearest_tau = find_nearest(a=self.vol_backbone.index.to_numpy(), value=tau, is_equal_or_largest=True)
|
|
43
|
+
vol_backbone_eta = self.vol_backbone.loc[nearest_tau]
|
|
44
|
+
else:
|
|
45
|
+
vol_backbone_eta = 1.0
|
|
46
|
+
return vol_backbone_eta
|
|
47
|
+
|
|
48
|
+
def get_vol_backbone_etas(self, ttms: np.ndarray) -> np.ndarray:
|
|
49
|
+
if self.vol_backbone is not None:
|
|
50
|
+
vol_backbone_etas = np.ones_like(ttms)
|
|
51
|
+
for idx, tau in enumerate(ttms):
|
|
52
|
+
nearest_tau = find_nearest(a=self.vol_backbone.index.to_numpy(), value=tau, is_equal_or_largest=True)
|
|
53
|
+
vol_backbone_etas[idx] = self.vol_backbone.loc[nearest_tau]
|
|
54
|
+
else:
|
|
55
|
+
vol_backbone_etas = np.ones_like(ttms)
|
|
56
|
+
return vol_backbone_etas
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def kappa(self) -> float:
|
|
60
|
+
return self.kappa1+self.kappa2*self.theta
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def theta2(self) -> float:
|
|
64
|
+
return self.theta*self.theta
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def vartheta2(self) -> float:
|
|
68
|
+
return self.beta*self.beta + self.volvol*self.volvol
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def gamma(self) -> float:
|
|
72
|
+
"""
|
|
73
|
+
assume kappa2 = kappa1 / theta
|
|
74
|
+
"""
|
|
75
|
+
return self.kappa1 / self.theta
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def eta(self) -> float:
|
|
79
|
+
"""
|
|
80
|
+
assume kappa2 = kappa1 / theta
|
|
81
|
+
"""
|
|
82
|
+
return self.kappa1 * self.theta / self.vartheta2 - 1.0
|
|
83
|
+
|
|
84
|
+
def get_x_grid(self, ttm: float = 1.0, n_stdevs: float = 3.0, n: int = 200) -> np.ndarray:
|
|
85
|
+
"""
|
|
86
|
+
spacial grid to compute density of x
|
|
87
|
+
"""
|
|
88
|
+
sigma_t = np.sqrt(ttm * 0.5 * (np.square(self.sigma0) + np.square(self.theta)))
|
|
89
|
+
drift = - 0.5*sigma_t*sigma_t
|
|
90
|
+
stdev = (n_stdevs+1)*sigma_t
|
|
91
|
+
return np.linspace(-stdev+drift, stdev+drift, n)
|
|
92
|
+
|
|
93
|
+
def get_sigma_grid(self, ttm: float = 1.0, n_stdevs: float = 3.0, n: int = 200) -> np.ndarray:
|
|
94
|
+
"""
|
|
95
|
+
spacial grid to compute density of sigma
|
|
96
|
+
"""
|
|
97
|
+
sigma_t = np.sqrt(0.5*(np.square(self.sigma0) + np.square(self.theta)))
|
|
98
|
+
vvol = 0.5*np.sqrt(self.vartheta2*ttm)
|
|
99
|
+
return np.linspace(0.0, sigma_t+n_stdevs*vvol, n)
|
|
100
|
+
|
|
101
|
+
def get_qvar_grid(self, ttm: float = 1.0, n_stdevs: float = 3.0, n: int = 200) -> np.ndarray:
|
|
102
|
+
"""
|
|
103
|
+
spacial grid to compute density of i
|
|
104
|
+
"""
|
|
105
|
+
sigma_t = np.sqrt(ttm * (np.square(self.sigma0) + np.square(self.theta)))
|
|
106
|
+
vvol = np.sqrt(self.vartheta2)*ttm
|
|
107
|
+
return np.linspace(0.0, sigma_t+n_stdevs*vvol, n)
|
|
108
|
+
|
|
109
|
+
def get_variable_space_grid(self, variable_type: VariableType = VariableType.LOG_RETURN,
|
|
110
|
+
ttm: float = 1.0,
|
|
111
|
+
n_stdevs: float = 3,
|
|
112
|
+
n: int = 200
|
|
113
|
+
) -> np.ndarray:
|
|
114
|
+
if variable_type == VariableType.LOG_RETURN:
|
|
115
|
+
return self.get_x_grid(ttm=ttm, n_stdevs=n_stdevs, n=n)
|
|
116
|
+
if variable_type == VariableType.SIGMA:
|
|
117
|
+
return self.get_sigma_grid(ttm=ttm, n_stdevs=n_stdevs, n=n)
|
|
118
|
+
elif variable_type == VariableType.Q_VAR:
|
|
119
|
+
return self.get_qvar_grid(ttm=ttm, n_stdevs=n_stdevs, n=n)
|
|
120
|
+
else:
|
|
121
|
+
raise NotImplementedError
|
|
122
|
+
|
|
123
|
+
def get_vol_moments_lambda(self,
|
|
124
|
+
n_terms: int = 4
|
|
125
|
+
) -> np.ndarray:
|
|
126
|
+
|
|
127
|
+
kappa2 = self.kappa2
|
|
128
|
+
kappa = self.kappa
|
|
129
|
+
vartheta2 = self.vartheta2
|
|
130
|
+
theta = self.theta
|
|
131
|
+
theta2 = self.theta2
|
|
132
|
+
|
|
133
|
+
def c(n: int) -> float:
|
|
134
|
+
return 0.5 * vartheta2 * n * (n - 1.0)
|
|
135
|
+
|
|
136
|
+
lambda_m = np.zeros((n_terms, n_terms))
|
|
137
|
+
lambda_m[0, 0] = -kappa
|
|
138
|
+
lambda_m[0, 1] = -kappa2
|
|
139
|
+
lambda_m[1, 0] = 2.0*c(2) * theta
|
|
140
|
+
lambda_m[1, 1] = c(2) - 2.0*kappa
|
|
141
|
+
lambda_m[1, 2] = -2.0*kappa2
|
|
142
|
+
|
|
143
|
+
for n_ in np.arange(2, n_terms):
|
|
144
|
+
n = n_ + 1 # n_ is array counter, n is formula counter
|
|
145
|
+
c_n = c(n)
|
|
146
|
+
lambda_m[n_, n_ - 2] = c_n * theta2
|
|
147
|
+
lambda_m[n_, n_ - 1] = 2.0 * c_n * theta
|
|
148
|
+
lambda_m[n_, n_] = c_n - n*kappa
|
|
149
|
+
if n_ + 1 < n_terms:
|
|
150
|
+
lambda_m[n_, n_ + 1] = -n*kappa2
|
|
151
|
+
|
|
152
|
+
return lambda_m
|
|
153
|
+
|
|
154
|
+
def assert_vol_moments_stability(self, n_terms: int = 4):
|
|
155
|
+
lambda_m = self.get_vol_moments_lambda(n_terms=n_terms)
|
|
156
|
+
w, v = la.eig(lambda_m)
|
|
157
|
+
cond = np.all(np.real(w)<0.0)
|
|
158
|
+
print(f"vol moments stable = {cond}")
|
|
159
|
+
|
|
160
|
+
def print_vol_moments_stability(self, n_terms: int = 4) -> None:
|
|
161
|
+
def c(n: int) -> float:
|
|
162
|
+
return 0.5 * self.vartheta2 * n * (n - 1.0)
|
|
163
|
+
|
|
164
|
+
cond_m2 = c(2) - 2.0*self.kappa
|
|
165
|
+
print(f"con2:\n{cond_m2}")
|
|
166
|
+
cond_m3 = c(3) - 3.0*self.kappa
|
|
167
|
+
print(f"con3:\n{cond_m3}")
|
|
168
|
+
cond_m4 = c(4) - 4.0*self.kappa
|
|
169
|
+
print(f"cond4:\n{cond_m4}")
|
|
170
|
+
|
|
171
|
+
lambda_m = self.get_vol_moments_lambda(n_terms=n_terms)
|
|
172
|
+
print(f"lambda_m:\n{lambda_m}")
|
|
173
|
+
|
|
174
|
+
w, v = la.eig(lambda_m)
|
|
175
|
+
print(f"eigenvalues w:\n{w}")
|
|
176
|
+
print(f"vol moments stable = {np.all(np.real(w)<0.0)}")
|
{stochvolmodels-1.0.20 → stochvolmodels-1.0.22}/stochvolmodels/pricers/logsv/vol_moments_ode.py
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
analytics for vol and QV moments computation
|
|
3
3
|
"""
|
|
4
|
-
|
|
4
|
+
# packages
|
|
5
5
|
import numpy as np
|
|
6
6
|
import pandas as pd
|
|
7
7
|
import matplotlib.pyplot as plt
|
|
@@ -9,9 +9,9 @@ import seaborn as sns
|
|
|
9
9
|
from numpy import linalg as la
|
|
10
10
|
from scipy import linalg as sla
|
|
11
11
|
from enum import Enum
|
|
12
|
-
|
|
13
|
-
from
|
|
14
|
-
from
|
|
12
|
+
# project
|
|
13
|
+
from stochvolmodels.pricers.logsv.logsv_params import LogSvParams
|
|
14
|
+
from stochvolmodels.utils.funcs import set_seed
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
VOLVOL = 1.75
|
|
@@ -40,7 +40,7 @@ def compute_analytic_vol_moments(params: LogSvParams,
|
|
|
40
40
|
rhs = np.zeros(n_terms)
|
|
41
41
|
rhs[1] = params.vartheta2*params.theta2
|
|
42
42
|
|
|
43
|
-
if is_qvar:
|
|
43
|
+
if is_qvar: # need flat boundary condition
|
|
44
44
|
rhs[-1] = -n_terms*params.kappa2*np.power(y, n_terms+1)
|
|
45
45
|
else:
|
|
46
46
|
rhs[-1] = -n_terms*params.kappa2*np.power(y, n_terms+1)
|
|
@@ -73,6 +73,9 @@ def compute_analytic_qvar(params: LogSvParams,
|
|
|
73
73
|
ttm: float = 1.0,
|
|
74
74
|
n_terms: int = 4
|
|
75
75
|
) -> float:
|
|
76
|
+
"""
|
|
77
|
+
compute expected value [ (1/T) int^T_0 sigma^2_t dt]
|
|
78
|
+
"""
|
|
76
79
|
if np.isclose(ttm, 0.0):
|
|
77
80
|
qvar = np.square(params.sigma0)
|
|
78
81
|
else:
|
|
@@ -113,10 +116,45 @@ def compute_sqrt_qvar_t(params: LogSvParams, t: np.ndarray, n_terms: int = 4) ->
|
|
|
113
116
|
return ev
|
|
114
117
|
|
|
115
118
|
|
|
119
|
+
def fit_model_vol_backbone_to_varswaps(log_sv_params: LogSvParams,
|
|
120
|
+
varswap_strikes: pd.Series,
|
|
121
|
+
n_terms: int = 4,
|
|
122
|
+
verbose: bool = False
|
|
123
|
+
) -> pd.Series:
|
|
124
|
+
"""
|
|
125
|
+
fit model eta so that model reproduces quadratic var
|
|
126
|
+
"""
|
|
127
|
+
ttms = varswap_strikes.index.to_numpy()
|
|
128
|
+
market_qvar_dt = ttms * np.square(varswap_strikes.to_numpy())
|
|
129
|
+
# compute model qvars
|
|
130
|
+
model_forwards = np.array([compute_analytic_qvar(params=log_sv_params, ttm=ttm, n_terms=n_terms) for ttm in ttms])
|
|
131
|
+
model_qvar_dt = model_forwards*ttms
|
|
132
|
+
model_eta = np.ones_like(ttms)
|
|
133
|
+
for idx, ttm in enumerate(ttms):
|
|
134
|
+
if idx == 0:
|
|
135
|
+
model_eta[idx] = market_qvar_dt[idx] / model_qvar_dt[idx]
|
|
136
|
+
else:
|
|
137
|
+
model_eta[idx] = (market_qvar_dt[idx]-market_qvar_dt[idx-1]) / (model_qvar_dt[idx]-model_qvar_dt[idx-1])
|
|
138
|
+
# model_eta = np.where(model_eta > 0.0, np.sqrt(model_eta), 1.0)
|
|
139
|
+
model_eta = np.where(model_eta > 0.0, model_eta, 1.0)
|
|
140
|
+
# adhoc adjustemnt for now
|
|
141
|
+
model_eta = np.where(ttms < 0.06, np.sqrt(model_eta), model_eta)
|
|
142
|
+
|
|
143
|
+
model_eta = pd.Series(model_eta, index=ttms)
|
|
144
|
+
if verbose:
|
|
145
|
+
varswap_strikes = np.sqrt(varswap_strikes.to_frame('vars_swap strikes'))
|
|
146
|
+
varswap_strikes['market_qvar_dt'] = market_qvar_dt
|
|
147
|
+
varswap_strikes['model_qvar_dt'] = model_qvar_dt
|
|
148
|
+
varswap_strikes['model_eta'] = model_eta
|
|
149
|
+
print(f"vars_swaps\n{varswap_strikes}")
|
|
150
|
+
return model_eta
|
|
151
|
+
|
|
152
|
+
|
|
116
153
|
class UnitTests(Enum):
|
|
117
154
|
VOL_MOMENTS = 1
|
|
118
155
|
EXPECTED_VOL = 2
|
|
119
156
|
EXPECTED_QVAR = 3
|
|
157
|
+
VOL_BACKBONE = 4
|
|
120
158
|
|
|
121
159
|
|
|
122
160
|
def run_unit_test(unit_test: UnitTests):
|
|
@@ -185,12 +223,17 @@ def run_unit_test(unit_test: UnitTests):
|
|
|
185
223
|
sns.lineplot(data=analytic_vol_moments, dashes=False, ax=ax)
|
|
186
224
|
ax.errorbar(x=df.index[::5], y=mc_mean[::5], yerr=mc_std[::5], fmt='o', color='green', capsize=8)
|
|
187
225
|
|
|
226
|
+
elif unit_test == UnitTests.VOL_BACKBONE:
|
|
227
|
+
fit_model_vol_backbone_to_varswaps(log_sv_params=params,
|
|
228
|
+
varswap_strikes=pd.Series([1.0, 1.0], index=[1.0 / 12., 2 / 12.0]),
|
|
229
|
+
verbose=True)
|
|
230
|
+
|
|
188
231
|
plt.show()
|
|
189
232
|
|
|
190
233
|
|
|
191
234
|
if __name__ == '__main__':
|
|
192
235
|
|
|
193
|
-
unit_test = UnitTests.
|
|
236
|
+
unit_test = UnitTests.VOL_BACKBONE
|
|
194
237
|
|
|
195
238
|
is_run_all_tests = False
|
|
196
239
|
if is_run_all_tests:
|