├── .gitignore ├── LICENSE.txt ├── README.md ├── docs └── figures │ ├── btc_fit.PNG │ └── btc_mc_comp.PNG ├── my_papers ├── forward_var │ └── calibrate_forward_var.py ├── il_hedging │ ├── README.md │ ├── logsv_figures.py │ └── run_logsv_for_il_payoff.py ├── inverse_options │ ├── README.md │ └── compare_net_delta.py ├── logsv_model_wtih_quadratic_drift │ ├── README.md │ ├── article_figures.py │ ├── calibrations.py │ ├── compare_admis_reg.py │ ├── model_fit_to_options_timeseries.py │ ├── moments_vol_qvar.py │ ├── ode_sol_in_time.py │ ├── steady_state_pdf.py │ └── vol_drift.py ├── risk_premia_gmm │ ├── check_kernel.py │ ├── gmm_slides.py │ ├── plot_gmm.py │ ├── q_kernel.py │ └── run_gmm_fit.py ├── sv_for_factor_hjm │ ├── README.md │ ├── calibration_fig_5_6_7.py │ └── calibration_fig_8_9.py ├── t_distribution │ ├── illustrations.py │ ├── market_data_fit.py │ └── mc_pricer_with_kernel.py └── volatility_models │ ├── README.md │ ├── article_figures.py │ ├── autocorr_fit.py │ ├── load_data.py │ ├── ss_distribution_fit.py │ └── vol_beta.py ├── pyproject.toml ├── read_modules.py ├── requirements.txt ├── resources ├── BTC.csv └── ETH.csv ├── setup.cfg ├── setup.py ├── stochvolmodels ├── __init__.py ├── data │ ├── __init__.py │ ├── fetch_option_chain.py │ ├── option_chain.py │ └── test_option_chain.py ├── examples │ ├── quick_run_lognormal_sv_pricer.py │ ├── run_heston.py │ ├── run_heston_sv_pricer.py │ ├── run_lognormal_sv_pricer.py │ └── run_pricing_options_on_qvar.py ├── pricers │ ├── __init__.py │ ├── analytic │ │ ├── __init__.py │ │ ├── bachelier.py │ │ ├── bsm.py │ │ └── tdist.py │ ├── factor_hjm │ │ ├── double_exp_pricer.py │ │ ├── factor_hjm_pricer.py │ │ ├── rate_affine_expansion.py │ │ ├── rate_core.py │ │ ├── rate_evaluate.py │ │ ├── rate_factor_basis.py │ │ ├── rate_logsv_ivols.py │ │ ├── rate_logsv_params.py │ │ └── rate_logsv_pricer.py │ ├── gmm_pricer.py │ ├── hawkes_jd_pricer.py │ ├── heston_pricer.py │ ├── logsv │ │ ├── __init__.py │ │ ├── affine_expansion.py │ │ ├── logsv_params.py │ │ ├── rough_kernel_approx.py │ │ └── vol_moments_ode.py │ ├── logsv_pricer.py │ ├── model_pricer.py │ └── tdist_pricer.py ├── tests │ ├── __init__.py │ ├── bsm_mgf_pricer.py │ └── qv_pricer.py └── utils │ ├── __init__.py │ ├── config.py │ ├── funcs.py │ ├── mc_payoffs.py │ ├── mgf_pricer.py │ ├── plots.py │ └── var_swap_pricer.py └── volatility_book ├── __init__.py └── ch_lognormal_sv ├── __init__.py └── quadratic_var.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | docs/figures/ 3 | my_projects/ 4 | 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | .idea/ 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | -------------------------------------------------------------------------------- /docs/figures/btc_fit.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/StochVolModels/228ed94afd25a561f01afb5c1c7c5160454dc9f1/docs/figures/btc_fit.PNG -------------------------------------------------------------------------------- /docs/figures/btc_mc_comp.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/StochVolModels/228ed94afd25a561f01afb5c1c7c5160454dc9f1/docs/figures/btc_mc_comp.PNG -------------------------------------------------------------------------------- /my_papers/forward_var/calibrate_forward_var.py: -------------------------------------------------------------------------------- 1 | # packages 2 | import numpy as np 3 | import pandas as pd 4 | import matplotlib.pyplot as plt 5 | from enum import Enum 6 | 7 | import qis 8 | 9 | # project 10 | import stochvolmodels as sv 11 | from stochvolmodels.pricers.logsv_pricer import LogSVPricer, LogsvModelCalibrationType 12 | from stochvolmodels.pricers.logsv.vol_moments_ode import fit_model_vol_backbone_to_varswaps 13 | from stochvolmodels import LogSvParams 14 | from stochvolmodels.utils.funcs import set_seed 15 | 16 | 17 | class UnitTests(Enum): 18 | VARSWAP_FIT = 1 19 | CALIBRATE_4PARAM_MODEL = 2 20 | CALIBRATE_VARSWAP_PARAM_MODEL = 3 21 | COMPARE_MODEL_VOLS_TO_MC = 4 22 | 23 | 24 | def run_unit_test(unit_test: UnitTests): 25 | 26 | set_seed(24) 27 | 28 | logsv_pricer = LogSVPricer() 29 | option_chain = sv.get_btc_test_chain_data() 30 | 31 | local_path = "C://Users//artur//OneDrive//My Papers//MyPresentations//Crypto Vols Tartu. Zurich. Aug 2022//figures//" 32 | 33 | if unit_test == UnitTests.VARSWAP_FIT: 34 | btc_log_params = LogSvParams(sigma0=0.7118361434192538, theta=0.7118361434192538, 35 | kappa1=2.214702576955766, kappa2=2.18028273418397, beta=0.0, 36 | volvol=0.921487415907961) 37 | btc_log_params = LogSvParams(sigma0=0.88, theta=0.88, 38 | kappa1=2.214702576955766, kappa2=2.18028273418397, beta=0.0, 39 | volvol=0.921487415907961) 40 | 41 | vars_swaps = option_chain.get_slice_varswap_strikes() 42 | vars_swaps1 = pd.Series(np.square(option_chain.get_chain_atm_vols()), index=option_chain.ttms) 43 | vars_swaps = np.maximum(vars_swaps, vars_swaps1) 44 | 45 | vol_backbone = fit_model_vol_backbone_to_varswaps(log_sv_params=btc_log_params, 46 | varswap_strikes=vars_swaps, 47 | verbose=True) 48 | btc_log_params.set_vol_backbone(vol_backbone=vol_backbone) 49 | 50 | logsv_pricer = LogSVPricer() 51 | logsv_pricer.plot_model_ivols_vs_bid_ask(option_chain=option_chain, 52 | params=btc_log_params) 53 | 54 | elif unit_test == UnitTests.CALIBRATE_4PARAM_MODEL: 55 | params0 = LogSvParams(sigma0=0.8, theta=1.0, kappa1=2.21, kappa2=2.18, beta=0.15, volvol=2.0) 56 | fitted_params = LogSvParams(sigma0=0.8626, theta=1.0417, kappa1=2.21, kappa2=2.18, beta=0.13, volvol=1.6286) 57 | btc_calibrated_params = fitted_params 58 | """ 59 | btc_calibrated_params = logsv_pricer.calibrate_model_params_to_chain(option_chain=option_chain, 60 | params0=params0, 61 | model_calibration_type=LogsvModelCalibrationType.PARAMS4, 62 | constraints_type=sv.ConstraintsType.INVERSE_MARTINGALE) 63 | """ 64 | print(btc_calibrated_params) 65 | fig = logsv_pricer.plot_model_ivols_vs_bid_ask(option_chain=option_chain, 66 | params=btc_calibrated_params) 67 | qis.save_fig(fig=fig, file_name='four_param_model_fit', local_path=local_path) 68 | 69 | elif unit_test == UnitTests.CALIBRATE_VARSWAP_PARAM_MODEL: 70 | params0 = LogSvParams(sigma0=0.85, theta=0.85, kappa1=2.21, kappa2=2.18, beta=0.15, volvol=1.5) 71 | fitted_params = LogSvParams(sigma0=0.85, theta=1.0, kappa1=2.21, kappa2=2.18, beta=0.24, volvol=1.14) 72 | btc_calibrated_params = logsv_pricer.calibrate_model_params_to_chain( 73 | option_chain=option_chain, 74 | params0=params0, 75 | params_min=LogSvParams(sigma0=0.1, theta=0.1, kappa1=0.25, kappa2=0.25, beta=0.0, volvol=1.5), 76 | model_calibration_type=LogsvModelCalibrationType.PARAMS_WITH_VARSWAP_FIT, 77 | constraints_type=sv.ConstraintsType.INVERSE_MARTINGALE) 78 | print(btc_calibrated_params) 79 | 80 | fig = logsv_pricer.plot_model_ivols_vs_bid_ask(option_chain=option_chain, 81 | params=btc_calibrated_params) 82 | qis.save_fig(fig=fig, file_name='backbone_model_fit', local_path=local_path) 83 | 84 | elif unit_test == UnitTests.COMPARE_MODEL_VOLS_TO_MC: 85 | uniform_chain_data = sv.OptionChain.to_uniform_strikes(obj=option_chain, num_strikes=31) 86 | is_varswap = True 87 | if is_varswap: 88 | fitted_params = LogSvParams(sigma0=0.85, theta=.85, kappa1=2.21, kappa2=2.18, beta=0.24, volvol=1.14) 89 | varswap_strikes = option_chain.get_slice_varswap_strikes(floor_with_atm_vols=True) 90 | fitted_params.set_vol_backbone(vol_backbone=fit_model_vol_backbone_to_varswaps(log_sv_params=fitted_params, 91 | varswap_strikes=varswap_strikes)) 92 | else: 93 | fitted_params = LogSvParams(sigma0=0.8626, theta=1.0417, kappa1=2.21, kappa2=2.18, beta=0.13, volvol=1.6286) 94 | 95 | logsv_pricer.plot_model_ivols_vs_mc(option_chain=uniform_chain_data, 96 | params=fitted_params, 97 | nb_path=100000) 98 | 99 | logsv_pricer.plot_comp_mma_inverse_options_with_mc(option_chain=uniform_chain_data, 100 | params=fitted_params, 101 | nb_path=100000) 102 | 103 | plt.show() 104 | 105 | 106 | if __name__ == '__main__': 107 | 108 | unit_test = UnitTests.CALIBRATE_VARSWAP_PARAM_MODEL 109 | 110 | is_run_all_tests = False 111 | if is_run_all_tests: 112 | for unit_test in UnitTests: 113 | run_unit_test(unit_test=unit_test) 114 | else: 115 | run_unit_test(unit_test=unit_test) 116 | -------------------------------------------------------------------------------- /my_papers/il_hedging/README.md: -------------------------------------------------------------------------------- 1 | This module contains analysis for paper 2 | [Unified Approach for Hedging Impermanent Loss of Liquidity Provision](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4887298) 3 | by Artur Sepp, Alexander Lipton, and Vladimir Lucic 4 | 5 | All figures in the paper are produced by unittests in 6 | https://github.com/ArturSepp/StochVolModels/blob/main/my_papers/il_hedging/run_logsv_for_il_payoff.py 7 | 8 | See the description of data and analysis in the paper. -------------------------------------------------------------------------------- /my_papers/il_hedging/logsv_figures.py: -------------------------------------------------------------------------------- 1 | """ 2 | run few unit test to illustrate implementation of log-normal sv model analytics 3 | """ 4 | 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | import seaborn as sns 8 | from enum import Enum 9 | 10 | from stochvolmodels import LogSVPricer, LogSvParams, OptionChain 11 | 12 | 13 | def plot_skews(): 14 | logsv_pricer = LogSVPricer() 15 | option_chain = OptionChain.get_uniform_chain(ttms=np.array([14.0/365.0]), 16 | ids=np.array(['2w']), 17 | strikes=np.linspace(0.6, 1.4, 21)) 18 | 19 | # define parameters for bootstrap 20 | sigma0 = 0.5 21 | params_dict = {'beta=-1': LogSvParams(sigma0=sigma0, theta=sigma0, kappa1=5.0, kappa2=5.0, beta=-1, volvol=1.0), 22 | 'beta=0': LogSvParams(sigma0=sigma0, theta=sigma0, kappa1=5.0, kappa2=5.0, beta=0.0, volvol=1.4), 23 | 'beta=1': LogSvParams(sigma0=sigma0, theta=sigma0, kappa1=5.0, kappa2=5.0, beta=1.0, volvol=1.0)} 24 | 25 | params_dict = { 26 | 'volvol=1.0': LogSvParams(sigma0=sigma0, theta=sigma0, kappa1=2.21, kappa2=2.18, beta=0.0, volvol=1.0), 27 | 'volvol=2.0': LogSvParams(sigma0=sigma0 - 0.005, theta=sigma0 - 0.005, kappa1=2.21, kappa2=2.18, beta=0.0, 28 | volvol=2.0), 29 | 'volvol=3.0': LogSvParams(sigma0=sigma0 - 0.01, theta=sigma0 - 0.01, kappa1=2.21, kappa2=2.18, beta=0.0, 30 | volvol=3.0)} 31 | 32 | # get slice for illustration 33 | option_slice = option_chain.get_slice(id='2w') 34 | logsv_pricer.plot_model_slices_in_params(option_slice=option_slice, 35 | params_dict=params_dict) 36 | 37 | 38 | class UnitTests(Enum): 39 | PLOT_SKEWS = 1 40 | 41 | 42 | def run_unit_test(unit_test: UnitTests): 43 | 44 | if unit_test == UnitTests.PLOT_SKEWS: 45 | with sns.axes_style("darkgrid"): 46 | fig, axs = plt.subplots(3, 1, figsize=(10, 7)) 47 | plot_skews() 48 | 49 | plt.show() 50 | 51 | 52 | if __name__ == '__main__': 53 | 54 | unit_test = UnitTests.PLOT_SKEWS 55 | 56 | is_run_all_tests = False 57 | if is_run_all_tests: 58 | for unit_test in UnitTests: 59 | run_unit_test(unit_test=unit_test) 60 | else: 61 | run_unit_test(unit_test=unit_test) 62 | -------------------------------------------------------------------------------- /my_papers/il_hedging/run_logsv_for_il_payoff.py: -------------------------------------------------------------------------------- 1 | """ 2 | computation of il payoff under log sv 3 | """ 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | from numba import njit 7 | from enum import Enum 8 | 9 | # stochvolmodels pricers 10 | from stochvolmodels import (get_transform_var_grid, 11 | vanilla_slice_pricer_with_mgf_grid, 12 | digital_slice_pricer_with_mgf_grid, 13 | compute_integration_weights, 14 | ExpansionOrder, 15 | get_expansion_n, 16 | compute_logsv_a_mgf_grid, 17 | LogSvParams) 18 | 19 | 20 | def logsv_il_pricer(params: LogSvParams, 21 | ttm: float, 22 | p1: float, # can price on range of forwards 23 | p0: float, 24 | pa: float, 25 | pb: float, 26 | is_stiff_solver: bool = False, 27 | expansion_order: ExpansionOrder = ExpansionOrder.SECOND, 28 | vol_scaler: float = None, 29 | notional: float = 1000000 30 | ) -> float: 31 | """ 32 | price il using bsm mgf 33 | """ 34 | # starting values 35 | if vol_scaler is None: # for calibrations we fix one vol_scaler so the grid is not affected by v0 36 | vol_scaler = params.sigma0 * np.sqrt(np.minimum(np.min(ttm), 0.5 / 12.0)) 37 | 38 | # for vanilla call and put 39 | phi_grid, psi_grid, theta_grid = get_transform_var_grid(vol_scaler=vol_scaler, 40 | real_phi=-0.4) 41 | a_t0 = np.zeros((phi_grid.shape[0], get_expansion_n(expansion_order)), dtype=np.complex128) 42 | 43 | a_t0, log_mgf_grid = compute_logsv_a_mgf_grid(ttm=ttm, 44 | phi_grid=phi_grid, 45 | psi_grid=psi_grid, 46 | theta_grid=theta_grid, 47 | a_t0=a_t0, 48 | expansion_order=expansion_order, 49 | is_stiff_solver=is_stiff_solver, 50 | **params.to_dict()) 51 | 52 | vanilla_option_prices = vanilla_slice_pricer_with_mgf_grid(log_mgf_grid=log_mgf_grid, 53 | phi_grid=phi_grid, 54 | forward=p1, 55 | strikes=np.array([pa, pb]), 56 | optiontypes=np.array(['P', 'C']), 57 | discfactor=1.0) 58 | bsm_put, bsm_call = vanilla_option_prices[0], vanilla_option_prices[1] 59 | 60 | digital_options = digital_slice_pricer_with_mgf_grid(log_mgf_grid=log_mgf_grid, 61 | phi_grid=phi_grid, 62 | forward=p1, 63 | strikes=np.array([pa, pb]), 64 | optiontypes=np.array(['P', 'C']), 65 | discfactor=1.0) 66 | digital_put, digital_call = digital_options[0], digital_options[1] 67 | 68 | square_root = square_root_payoff_pricer_with_mgf_grid(log_mgf_grid=log_mgf_grid, 69 | phi_grid=phi_grid, 70 | forward=p1, 71 | pa=pa, 72 | pb=pb, 73 | discfactor=1.0) 74 | 75 | sp0 = np.sqrt(p0) 76 | spa = np.sqrt(pa) 77 | spb = np.sqrt(pb) 78 | 79 | linear = sp0*(p1/p0+1.0) 80 | 81 | payoff = -2.0*square_root + linear + \ 82 | (1.0 / spa) * bsm_put - (1.0 / spb) * bsm_call \ 83 | -2.0 * spa * digital_put -2.0 * spb * digital_call 84 | 85 | notional0 = 1.0 / (2.0*sp0-p0/spb-spa) 86 | payoff = - (notional0*notional)*payoff 87 | return payoff 88 | 89 | 90 | logsv_il_pricer_vector = np.vectorize(logsv_il_pricer, doc='Vectorized `logsv_il_pricer`') 91 | 92 | 93 | @njit(cache=False, fastmath=True) 94 | def square_root_payoff_pricer_with_mgf_grid(log_mgf_grid: np.ndarray, 95 | phi_grid: np.ndarray, 96 | forward: float, 97 | pa: float, 98 | pb: float, 99 | discfactor: float = 1.0, 100 | is_simpson: bool = True 101 | ) -> np.ndarray: 102 | """ 103 | generic function for pricing digital options on the spot given the mgf grid 104 | mgf in x is function defined on log-price transform phi grids 105 | transform variable is phi_grid = real_phi + i*p 106 | grid can be non-uniform 107 | we can use either positive or negative phi_real but not 108 | """ 109 | dp = compute_integration_weights(var_grid=phi_grid, is_simpson=is_simpson) 110 | 111 | x = np.log(forward) 112 | xa = np.log(pa) 113 | xb = np.log(pb) 114 | p_payoff = (np.exp( (phi_grid+0.5)*xb - phi_grid*x) - np.exp((phi_grid+0.5)*xa - phi_grid*x)) 115 | p_payoff = (dp / np.pi) * p_payoff / (phi_grid+0.5) 116 | option_price = discfactor * np.nansum(np.real(p_payoff*np.exp(log_mgf_grid))) 117 | return option_price 118 | 119 | 120 | class UnitTests(Enum): 121 | COMPUTE_MODEL_PRICES = 1 122 | 123 | 124 | def run_unit_test(unit_test: UnitTests): 125 | 126 | # define model params 127 | params = LogSvParams(sigma0=0.4861785891939535, theta=0.6176006871606874, kappa1=1.955809653686808, kappa2=1.978367101612294, beta=-0.26916969112829325, volvol=3.265815229306317) 128 | 129 | if unit_test == UnitTests.COMPUTE_MODEL_PRICES: 130 | payoff = logsv_il_pricer(params=params, 131 | ttm=10.0/365.0, 132 | p1=2200.0, 133 | p0=2200.0, 134 | pa=2000.0, 135 | pb=2400.0) 136 | print(payoff) 137 | 138 | plt.show() 139 | 140 | 141 | if __name__ == '__main__': 142 | 143 | unit_test = UnitTests.COMPUTE_MODEL_PRICES 144 | 145 | is_run_all_tests = False 146 | if is_run_all_tests: 147 | for unit_test in UnitTests: 148 | run_unit_test(unit_test=unit_test) 149 | else: 150 | run_unit_test(unit_test=unit_test) 151 | -------------------------------------------------------------------------------- /my_papers/inverse_options/README.md: -------------------------------------------------------------------------------- 1 | This module contains analysis for paper 2 | [Valuation and Hedging of Cryptocurrency Inverse Options](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4606748) by Artur Sepp and Vladimir Lucic 3 | 4 | Comparision of net deltas in the paper are generated using 5 | ```python 6 | compare_net_delta.py 7 | ``` 8 | https://github.com/ArturSepp/StochVolModels/tree/main/my_papers/inverse_options 9 | 10 | See the description of data and analysis in the paper. 11 | -------------------------------------------------------------------------------- /my_papers/inverse_options/compare_net_delta.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import matplotlib.pyplot as plt 4 | import seaborn as sns 5 | import qis as qis 6 | from enum import Enum 7 | 8 | 9 | from stochvolmodels import (compute_bsm_vanilla_grid_deltas, 10 | compute_bsm_forward_grid_prices, 11 | compute_bsm_vanilla_price, 12 | compute_bsm_vanilla_delta) 13 | 14 | 15 | def compare_net_deltas(ttm: float, 16 | forward: float, 17 | vol: float, 18 | strike_level: float = 1.0, 19 | optiontype: str = 'C', 20 | ax: plt.Subplot = None, 21 | **kwargs 22 | ): 23 | 24 | spot_drid = np.linspace(0.7*forward, 1.3*forward, 1000) 25 | strike = strike_level * forward 26 | 27 | option_prices = compute_bsm_forward_grid_prices(ttm=ttm, forwards=spot_drid, strike=strike, vol=vol, optiontype=optiontype) 28 | option_deltas = compute_bsm_vanilla_grid_deltas(ttm=ttm, forwards=spot_drid, strike=strike, vol=vol, optiontype=optiontype) 29 | option_net_delta = option_deltas - option_prices / spot_drid 30 | option_deltas = pd.Series(option_deltas, index=spot_drid, name='Black Delta') 31 | option_net_delta = pd.Series(option_net_delta, index=spot_drid, name='Net Delta') 32 | deltas = pd.concat([option_deltas, option_net_delta], axis=1) 33 | 34 | qis.plot_line(df=deltas, 35 | xvar_format='{:,.0f}', 36 | ylabel='BTC price', 37 | ax=ax, 38 | **kwargs) 39 | 40 | 41 | def compare_pnl(ttm: float, 42 | forward: float, 43 | vol: float, 44 | strike_level: float = 1.0, 45 | optiontype: str = 'C', 46 | is_btc_pnl: bool = True, 47 | ax: plt.Subplot = None, 48 | **kwargs 49 | ): 50 | 51 | spot_drid = np.linspace(0.7*forward, 1.3*forward, 10000) 52 | returns_grid = spot_drid / forward - 1.0 53 | strike = strike_level*forward 54 | 55 | option_price0 = compute_bsm_vanilla_price(ttm=ttm, forward=forward, strike=strike, vol=vol, optiontype=optiontype) 56 | option_delta0 = compute_bsm_vanilla_delta(ttm=ttm, forward=forward, strike=strike, vol=vol, optiontype=optiontype) 57 | option_net_delta0 = option_delta0 - option_price0 / forward 58 | 59 | # price return 60 | inverse_price_return = (spot_drid-forward) / spot_drid 61 | dt = 1.0 / 365.0 62 | option_prices = compute_bsm_forward_grid_prices(ttm=ttm-dt, forwards=spot_drid, strike=strike, vol=vol, optiontype=optiontype) 63 | option_pnl_btc = (option_price0/forward - option_prices/spot_drid) 64 | 65 | # black p&l 66 | pnl_btc = option_pnl_btc + option_delta0 * inverse_price_return 67 | if not is_btc_pnl: 68 | pnl_btc = pnl_btc * spot_drid 69 | pnl_btc_positive = spot_drid[pnl_btc >= 0.0] 70 | lower_be = pnl_btc_positive[0] / forward - 1.0 71 | upper_be = pnl_btc_positive[-1] / forward - 1.0 72 | pnl_btc = pd.Series(pnl_btc, index=returns_grid, name=f"Black Delta: breakevens=({lower_be:0.2%}, {upper_be:0.2%})") 73 | 74 | # net p&l 75 | pnl_btc_net_delta = option_pnl_btc + option_net_delta0 * inverse_price_return 76 | if not is_btc_pnl: 77 | pnl_btc_net_delta = pnl_btc_net_delta * spot_drid 78 | pnl_btc_net_delta_positive = spot_drid[pnl_btc_net_delta >= 0.0] 79 | lower_be = pnl_btc_net_delta_positive[0] / forward - 1.0 80 | upper_be = pnl_btc_net_delta_positive[-1] / forward - 1.0 81 | pnl_btc_net_delta = pd.Series(pnl_btc_net_delta, index=returns_grid, name=f"Net Delta : breakevens=({lower_be:0.2%}, {upper_be:0.2%})") 82 | 83 | pnls = pd.concat([pnl_btc, pnl_btc_net_delta], axis=1) 84 | 85 | if is_btc_pnl: 86 | ylabel = 'BTC P&L' 87 | yvar_format = '{:,.2f}' 88 | else: 89 | ylabel = 'USD P&L' 90 | yvar_format = '{:,.0f}' 91 | 92 | qis.plot_line(df=pnls, 93 | xvar_format='{:,.1%}', 94 | yvar_format=yvar_format, 95 | ylabel=ylabel, 96 | xlabel='BTC % change', 97 | ax=ax, 98 | **kwargs) 99 | 100 | 101 | class UnitTests(Enum): 102 | DELTA_COMP = 1 103 | PNL_COMP = 2 104 | 105 | 106 | def run_unit_test(unit_test: UnitTests): 107 | 108 | LOCAL_PATH = "C://Users//artur//OneDrive//My Papers//Working Papers//Crypto Options. Zurich. Oct 2022//FinalFigures//" 109 | 110 | kwargs = dict(fontsize=14, framealpha=0.9) 111 | 112 | ttm = 7.0 / 365.0 113 | 114 | if unit_test == UnitTests.DELTA_COMP: 115 | 116 | with sns.axes_style("darkgrid"): 117 | fig, axs = plt.subplots(1, 2, figsize=(14, 7), tight_layout=True) 118 | compare_net_deltas(ttm=ttm, forward=50000, vol=0.6, optiontype='C', 119 | strike_level=1.0, 120 | title='(A) Call Delta for K=100%*S_0', 121 | ax=axs[0], 122 | **kwargs) 123 | compare_net_deltas(ttm=ttm, forward=50000, vol=0.6, optiontype='P', 124 | strike_level=1.0, 125 | title='(B) Put Delta for K=100%*S_0', 126 | ax=axs[1], 127 | **kwargs) 128 | 129 | is_save = True 130 | if is_save: 131 | qis.save_fig(fig, file_name='delta_comp', local_path=LOCAL_PATH) 132 | qis.save_fig(fig, file_name='delta_comp', file_type=qis.FileTypes.EPS, dpi=1200, local_path=LOCAL_PATH) 133 | 134 | elif unit_test == UnitTests.PNL_COMP: 135 | 136 | with sns.axes_style("darkgrid"): 137 | fig, axs = plt.subplots(1, 2, figsize=(14, 7), tight_layout=True) 138 | compare_pnl(ttm=ttm, forward=50000, vol=0.6, optiontype='C', 139 | title='(A) ATM Call K=100%*F_0', 140 | y_limits=(-0.3, 0.05), 141 | ax=axs[0], 142 | **kwargs) 143 | compare_pnl(ttm=ttm, forward=50000, vol=0.6, optiontype='C', 144 | title='(B) ITM Call K=90%*F_0', 145 | strike_level=0.9, 146 | is_btc_pnl=True, 147 | y_limits=(-0.3, 0.05), 148 | ax=axs[1], 149 | **kwargs) 150 | 151 | is_save = True 152 | if is_save: 153 | qis.save_fig(fig, file_name='pnl_comp', local_path=LOCAL_PATH) 154 | qis.save_fig(fig, file_name='pnl_comp', file_type=qis.FileTypes.EPS, dpi=1200, local_path=LOCAL_PATH) 155 | 156 | plt.show() 157 | 158 | 159 | if __name__ == '__main__': 160 | 161 | unit_test = UnitTests.DELTA_COMP 162 | 163 | is_run_all_tests = False 164 | if is_run_all_tests: 165 | for unit_test in UnitTests: 166 | run_unit_test(unit_test=unit_test) 167 | else: 168 | run_unit_test(unit_test=unit_test) 169 | -------------------------------------------------------------------------------- /my_papers/logsv_model_wtih_quadratic_drift/README.md: -------------------------------------------------------------------------------- 1 | This module contains python code for the analysis and figures for paper 2 | [Log-normal Stochastic Volatility Model with Quadratic Drift](https://www.worldscientific.com/doi/10.1142/S0219024924500031) 3 | by Artur Sepp and Parviz Rakhmonov published in International Journal of Theoretical and Applied Finance, 2023, 26(8) 4 | 5 | 6 | 7 | See the description of data and analysis in the paper. 8 | 9 | Figures in the paper are generated using unittests in 10 | ```python 11 | article_figures.py 12 | ``` 13 | https://github.com/ArturSepp/StochVolModels/blob/main/my_papers/logsv_model_wtih_quadratic_drift/article_figures.py 14 | 15 | See the description of data and analysis in the paper. 16 | -------------------------------------------------------------------------------- /my_papers/logsv_model_wtih_quadratic_drift/calibrations.py: -------------------------------------------------------------------------------- 1 | """ 2 | figures 11, 12, 13 and data for table 1 3 | model calibration to implied volatilities of different assets 4 | """ 5 | 6 | import numpy as np 7 | import pandas as pd 8 | import matplotlib.pyplot as plt 9 | from enum import Enum 10 | 11 | from stochvolmodels.utils.config import VariableType 12 | from stochvolmodels.data.option_chain import OptionChain 13 | from stochvolmodels.pricers.logsv_pricer import LogSVPricer, LogsvModelCalibrationType, ConstraintsType 14 | from stochvolmodels import LogSvParams 15 | from stochvolmodels.pricers.logsv.vol_moments_ode import compute_analytic_qvar 16 | from stochvolmodels.utils.funcs import set_seed 17 | import stochvolmodels.utils.plots as plot 18 | import stochvolmodels.data.test_option_chain as chains 19 | 20 | 21 | class Assets(str, Enum): 22 | BTC = 'Bitcoin' 23 | VIX = 'Vix' 24 | GLD = 'Gold' 25 | SQQQ = '-3x Nasdaq' 26 | SPY = 'S&P500' 27 | 28 | 29 | CALIBRATED_PARAMS = { 30 | Assets.VIX: LogSvParams(sigma0=0.9767, theta=0.5641, kappa1=4.9067, kappa2=8.6985, beta=2.3425, volvol=1.0163), 31 | Assets.SQQQ: LogSvParams(sigma0=0.9114, theta=0.9390, kappa1=4.9544, kappa2=5.2762, beta=1.3215, volvol=0.9964), 32 | Assets.BTC: LogSvParams(sigma0=0.8327, theta=1.0139, kappa1=4.8609, kappa2=4.7940, beta=0.1988, volvol=2.3694), 33 | Assets.GLD: LogSvParams(sigma0=0.1505, theta=0.1994, kappa1=2.2062, kappa2=11.0630, beta=0.1547, volvol=2.8011), 34 | Assets.SPY: LogSvParams(sigma0=0.2270, theta=0.2616, kappa1=4.9325, kappa2=18.8550, beta=-1.8123, volvol=0.9832), 35 | } 36 | 37 | 38 | def get_asset_chain_data(asset: Assets = Assets.BTC) -> OptionChain: 39 | match asset: 40 | case Assets.BTC: 41 | option_chain = chains.get_btc_test_chain_data() 42 | case Assets.VIX: 43 | option_chain = chains.get_vix_test_chain_data() 44 | case Assets.GLD: 45 | option_chain = chains.get_gld_test_chain_data() 46 | case Assets.SQQQ: 47 | option_chain = chains.get_sqqq_test_chain_data() 48 | case Assets.SPY: 49 | option_chain = chains.get_spy_test_chain_data() 50 | case _: 51 | raise NotImplementedError(f"not implemented {asset}") 52 | return option_chain 53 | 54 | 55 | def calibrate_logsv_model(asset: Assets = Assets.BTC, 56 | model_calibration_type: LogsvModelCalibrationType = LogsvModelCalibrationType.PARAMS5 57 | ): 58 | match asset: 59 | case Assets.BTC: 60 | params0 = LogSvParams(sigma0=0.84, theta=1.04, kappa1=5.0, kappa2=None, beta=0.15, volvol=1.85) 61 | constraints_type = ConstraintsType.INVERSE_MARTINGALE 62 | case Assets.VIX: 63 | params0 = LogSvParams(sigma0=0.8, theta=0.6, kappa1=5.0, kappa2=None, beta=2.0, volvol=1.0) 64 | constraints_type = ConstraintsType.MMA_MARTINGALE_MOMENT4 65 | case Assets.GLD: 66 | params0 = LogSvParams(sigma0=0.1530, theta=0.1960, kappa1=2.2068, kappa2=11.2584, beta=0.1580, volvol=2.8022) 67 | constraints_type = ConstraintsType.UNCONSTRAINT 68 | case Assets.SQQQ: 69 | params0 = LogSvParams(sigma0=1.0, theta=1.0, kappa1=5.0, kappa2=None, beta=1.0, volvol=1.0) 70 | constraints_type = ConstraintsType.MMA_MARTINGALE_MOMENT4 71 | case Assets.SPY: 72 | params0 = LogSvParams(sigma0=0.2, theta=0.2, kappa1=5.0, kappa2=None, beta=-1.0, volvol=1.0) 73 | constraints_type = ConstraintsType.MMA_MARTINGALE_MOMENT4 74 | case _: 75 | raise NotImplementedError(f"not implemented {asset}") 76 | 77 | option_chain = get_asset_chain_data(asset=asset) 78 | logsv_pricer = LogSVPricer() 79 | fit_params = logsv_pricer.calibrate_model_params_to_chain(option_chain=option_chain, 80 | params0=params0, 81 | model_calibration_type=model_calibration_type, 82 | constraints_type=constraints_type) 83 | fit_params.print_vol_moments_stability() 84 | print(fit_params) 85 | fig = logsv_pricer.plot_model_ivols_vs_bid_ask(option_chain=option_chain, 86 | params=fit_params, 87 | headers=('(A)', '(B)', '(C)', '(D)')) 88 | 89 | return fig 90 | 91 | 92 | class UnitTests(Enum): 93 | CHAIN_DATA = 0 94 | CALIBRATION = 1 95 | MODEL_COMPARISION_WITH_MC = 2 96 | ALL_PARAMS_TABLE = 3 97 | PLOT_BTC_COMP_FOR_ARTICLE = 4 98 | PLOT_QVAR_FIGURE_FOR_ARTICLE = 5 99 | 100 | 101 | def run_unit_test(unit_test: UnitTests): 102 | 103 | set_seed(24) 104 | 105 | if unit_test == UnitTests.CHAIN_DATA: 106 | option_chain = get_asset_chain_data(asset=Assets.BTC) 107 | print(option_chain) 108 | atm_vols = option_chain.get_chain_atm_vols() 109 | print(atm_vols) 110 | 111 | elif unit_test == UnitTests.CALIBRATION: 112 | asset = Assets.BTC 113 | fig = calibrate_logsv_model(asset=asset) 114 | plot.save_fig(fig=fig, local_path='../../docs/figures//', file_name=f"calibration_{asset.value}") 115 | 116 | elif unit_test == UnitTests.MODEL_COMPARISION_WITH_MC: 117 | 118 | asset = Assets.BTC 119 | option_chain = get_asset_chain_data(asset=asset) 120 | 121 | params = CALIBRATED_PARAMS[asset] 122 | params.print_vol_moments_stability() 123 | 124 | logsv_pricer = LogSVPricer() 125 | # logsv_pricer.plot_model_ivols(option_chain=option_chain, params=params) 126 | # logsv_pricer.plot_model_ivols_vs_mc(option_chain=option_chain, params=params, nb_path=400000) 127 | uniform_chain_data = OptionChain.to_uniform_strikes(obj=option_chain, num_strikes=31) 128 | logsv_pricer.plot_comp_mma_inverse_options_with_mc(option_chain=uniform_chain_data, params=params, nb_path=400000) 129 | 130 | elif unit_test == UnitTests.ALL_PARAMS_TABLE: 131 | datas = {key.value: param.to_dict() for key, param in CALIBRATED_PARAMS.items()} 132 | df = pd.DataFrame.from_dict(datas) 133 | print(df) 134 | df.to_clipboard() 135 | 136 | elif unit_test == UnitTests.PLOT_BTC_COMP_FOR_ARTICLE: 137 | 138 | asset = Assets.BTC 139 | params = CALIBRATED_PARAMS[asset] 140 | option_chain = get_asset_chain_data(asset=asset) 141 | logsv_pricer = LogSVPricer() 142 | 143 | fig1 = logsv_pricer.plot_model_ivols_vs_bid_ask(option_chain=option_chain, 144 | params=params, 145 | headers=('(A)', '(B)', '(C)', '(D)')) 146 | 147 | uniform_chain_data = OptionChain.to_uniform_strikes(obj=option_chain, num_strikes=31) 148 | fig2 = logsv_pricer.plot_comp_mma_inverse_options_with_mc(option_chain=uniform_chain_data, 149 | params=params, 150 | nb_path=400000) 151 | 152 | is_save = False 153 | if is_save: 154 | plot.save_fig(fig=fig1, local_path='../../docs/figures//', file_name="btc_fit") 155 | plot.save_fig(fig=fig2, local_path='../../docs/figures//', file_name="btc_mc_comp") 156 | 157 | elif unit_test == UnitTests.PLOT_QVAR_FIGURE_FOR_ARTICLE: 158 | 159 | asset = Assets.BTC 160 | params = CALIBRATED_PARAMS[asset] 161 | logsv_pricer = LogSVPricer() 162 | 163 | # ttms = {'1m': 1.0/12.0, '6m': 0.5} 164 | ttms = {'1m': 1.0 / 12.0, '3m': 0.25} 165 | forwards = np.array([compute_analytic_qvar(params=params, ttm=ttm) for ttm in ttms.values()]) 166 | print(f"QV forwards = {forwards}") 167 | 168 | option_chain = chains.get_qv_options_test_chain_data() 169 | option_chain = OptionChain.get_slices_as_chain(option_chain, ids=list(ttms.keys())) 170 | option_chain.forwards = forwards # replace forwards to imply BSM vols 171 | 172 | set_seed(80) 173 | fig = logsv_pricer.plot_comp_mma_inverse_options_with_mc(option_chain=option_chain, 174 | params=params, 175 | is_plot_vols=True, 176 | variable_type=VariableType.Q_VAR, 177 | figsize=(18, 6), 178 | nb_path=100000) 179 | is_save = False 180 | if is_save: 181 | plot.save_fig(fig=fig, local_path='../../docs/figures//', file_name="model_vs_mc_qvar_logsv") 182 | 183 | else: 184 | raise NotImplementedError(f"not implemented {unit_test}") 185 | 186 | plt.show() 187 | 188 | 189 | if __name__ == '__main__': 190 | 191 | unit_test = UnitTests.CHAIN_DATA 192 | 193 | is_run_all_tests = False 194 | if is_run_all_tests: 195 | for unit_test in UnitTests: 196 | run_unit_test(unit_test=unit_test) 197 | else: 198 | run_unit_test(unit_test=unit_test) 199 | -------------------------------------------------------------------------------- /my_papers/logsv_model_wtih_quadratic_drift/compare_admis_reg.py: -------------------------------------------------------------------------------- 1 | """ 2 | figures 2 and 3 in the logsv_model_wtih_quadratic_drift 3 | plot admissible regions for model parameters 4 | """ 5 | 6 | import numpy as np 7 | import matplotlib.pyplot as plt 8 | from enum import Enum 9 | 10 | import stochvolmodels.utils.plots as plot 11 | 12 | 13 | def lognormal_combined(vartheta_min=0.5, 14 | vartheta_max=3.0, 15 | beta_min=-2.5, 16 | beta_max=2.5, 17 | kappa2s=[3.0, 0.0]): 18 | hatch1 = '\\\\\\\\' 19 | hatch2 = '////' 20 | vartheta = np.linspace(vartheta_min, vartheta_max, 100) 21 | nb_kappas = len(kappa2s) 22 | moment = 2 23 | num = 97 24 | 25 | fig, ax = plt.subplots(1, nb_kappas, figsize=(4 * nb_kappas, 3), tight_layout=True) 26 | for idx, kappa2 in enumerate(kappa2s): 27 | # plot for bounds ensuring martingale property under spot and inverse spot 28 | beta_spot_meas = np.maximum(kappa2, beta_min) 29 | beta_spot_meas = np.ones_like(vartheta) * beta_spot_meas 30 | beta_inv_meas = np.maximum(0.5 * kappa2, beta_min) 31 | beta_inv_meas = np.ones_like(vartheta) * beta_inv_meas 32 | outline, = ax[idx].plot(vartheta, beta_spot_meas, color='black', linewidth=0.8) 33 | ax[idx].fill_between(vartheta, beta_min, beta_spot_meas, 34 | edgecolor='black', hatch=hatch1, label='MMA', facecolor='none') 35 | outline, = ax[idx].plot(vartheta, beta_inv_meas, color='black', linewidth=0.8) 36 | ax[idx].fill_between(vartheta, beta_min, beta_inv_meas, 37 | edgecolor='grey', hatch=hatch2, label='Inverse', facecolor='none') 38 | ax[idx].set_ylim(beta_min, beta_max, auto=True) 39 | ax[idx].set_title(f"({chr(num).upper()}): $\kappa_2={kappa2}$") 40 | num = num + 1 41 | # # Right plot for bounds ensuring existence of second moment under spot and inverse spot 42 | # m = moment 43 | # beta_spot_meas = np.maximum((kappa2 - vartheta * np.sqrt(m ** 2 - m)) / m, beta_min) 44 | # beta_inv_meas = np.maximum((kappa2 - vartheta * np.sqrt(m ** 2 - m)) / (m + 1.0), beta_min) 45 | # outline, = ax[idx, 1].plot(vartheta, beta_spot_meas, color='black', linewidth=0.8) 46 | # ax[idx, 1].fill_between(vartheta, beta_min, beta_spot_meas, 47 | # edgecolor='black', hatch=hatch1, label='Spot', facecolor='none') 48 | # outline, = ax[idx, 1].plot(vartheta, beta_inv_meas, color='black', linewidth=0.8) 49 | # ax[idx, 1].fill_between(vartheta, beta_min, beta_inv_meas, 50 | # edgecolor='grey', hatch=hatch2, label='Inverse', facecolor='none') 51 | # ax[idx, 1].set_ylim(beta_min, beta_max, auto=True) 52 | # ax[idx, 1].set_title(f"({chr(num).upper()}): $\kappa_2={kappa2}$") 53 | # num = num + 1 54 | for i in range(nb_kappas): 55 | # for j in range(2): 56 | ax[i].legend() 57 | ax[i].set(xlabel=r"$\vartheta$", ylabel=r"$\beta$") 58 | 59 | plot.save_fig(fig=fig, local_path='../../docs/figures//', 60 | file_name='logsv_regions') 61 | 62 | 63 | def heston_exp_ou_combined(vartheta_min=0.5, 64 | vartheta_max=3.0, 65 | rho_min=-1.0, 66 | rho_max=1.0, 67 | kappa=1, 68 | theta=1): 69 | hatch1 = '\\\\\\\\' 70 | hatch2 = '////' 71 | vartheta = np.linspace(vartheta_min, vartheta_max, 100) 72 | 73 | fig, ax = plt.subplots(1, 2, figsize=(10, 3), tight_layout=True) 74 | # Left plot for bounds ensuring existence of second moment under spot and inverse spot 75 | kappa_adj_inv = np.maximum(kappa / vartheta, rho_min) 76 | outline, = ax[0].plot(vartheta, kappa_adj_inv, color='black', linewidth=0.8) 77 | ax[0].fill_between(vartheta, rho_min, kappa_adj_inv, 78 | edgecolor='black', hatch=hatch1, label=r"$\kappa>\rho\vartheta$", 79 | facecolor='none') 80 | critical_val = np.sqrt(2.0 * kappa * theta) 81 | ax[0].axvspan(vartheta_min, critical_val, edgecolor='black', hatch=hatch2, label='Feller', 82 | facecolor='none') 83 | ax[0].set_ylim(rho_min, rho_max, auto=True) 84 | ax[0].legend() 85 | ax[0].set(xlabel=r"$\vartheta$", ylabel=r"$\rho$") 86 | ax[0].set_title(f"(A) Heston model") 87 | # Right plot for bounds ensuring existence of second moment under spot and inverse spot 88 | kappa2 = 0 89 | rho_spot_meas = np.maximum(kappa2 / vartheta, rho_min) 90 | rho_inv_meas = np.maximum(0.5 * kappa2 / vartheta, rho_min) 91 | outline, = ax[1].plot(vartheta, rho_spot_meas, color='black', linewidth=0.8) 92 | ax[1].fill_between(vartheta, rho_min, rho_spot_meas, 93 | edgecolor='black', hatch=hatch1, label='MMA', 94 | facecolor='none') 95 | outline, = ax[1].plot(vartheta, rho_inv_meas, color='black', linewidth=0.8) 96 | ax[1].fill_between(vartheta, rho_min, rho_inv_meas, 97 | edgecolor='grey', hatch=hatch2, label='Inverse', 98 | facecolor='none') 99 | ax[1].set_ylim(rho_min, rho_max, auto=True) 100 | ax[1].legend() 101 | ax[1].set(xlabel=r"$\vartheta$", ylabel=r"$\rho$") 102 | ax[1].set_title(f"(B) Exp-OU model") 103 | 104 | plot.save_fig(fig=fig, local_path='../../docs/figures//', 105 | file_name='heston_exp_ou_combined') 106 | 107 | 108 | class UnitTests(Enum): 109 | LOGNORMAL_SV_COMBINED = 1 110 | HESTON_EXP_OU_COMBINED = 2 111 | 112 | 113 | def run_unit_test(unit_test: UnitTests): 114 | if unit_test == UnitTests.LOGNORMAL_SV_COMBINED: 115 | lognormal_combined(vartheta_min=0.5, 116 | vartheta_max=3.0, 117 | kappa2s=[0.0, 1.0]) 118 | 119 | elif unit_test == UnitTests.HESTON_EXP_OU_COMBINED: 120 | heston_exp_ou_combined(vartheta_min=0.5, 121 | vartheta_max=3.0) 122 | 123 | plt.show() 124 | 125 | 126 | if __name__ == '__main__': 127 | unit_test = UnitTests.LOGNORMAL_SV_COMBINED 128 | run_unit_test(unit_test=unit_test) 129 | -------------------------------------------------------------------------------- /my_papers/logsv_model_wtih_quadratic_drift/steady_state_pdf.py: -------------------------------------------------------------------------------- 1 | """ 2 | figure 5 in the logsv_model_wtih_quadratic_drift 3 | plot steady state pdfs 4 | """ 5 | 6 | import numpy as np 7 | import pandas as pd 8 | import matplotlib.pyplot as plt 9 | import seaborn as sns 10 | import scipy.special as sps 11 | import matplotlib.ticker as mticker 12 | from numba import njit 13 | from enum import Enum 14 | from typing import Dict 15 | 16 | from stochvolmodels import LogSvParams 17 | import stochvolmodels.utils.plots as plot 18 | 19 | VOLVOL = 1.5 20 | 21 | TEST_PARAMS = {'$(\kappa_{1}=4, \kappa_{2}=0); \sigma_{0}=2.0$': LogSvParams(sigma0=2.0, theta=1.0, kappa1=4.0, kappa2=0.0, beta=0.0, volvol=VOLVOL), 22 | '$(\kappa_{1}=4, \kappa_{2}=4); \sigma_{0}=2.0$': LogSvParams(sigma0=2.0, theta=1.0, kappa1=4.0, kappa2=4.0, beta=0.0, volvol=VOLVOL), 23 | '$(\kappa_{1}=4, \kappa_{2}=8); \sigma_{0}=2.0$': LogSvParams(sigma0=2.0, theta=1.0, kappa1=4.0, kappa2=8.0, beta=0.0, volvol=VOLVOL)} 24 | 25 | SS_PDF_PARAMS = {'$(\kappa_{1}=4, \kappa_{2}=0)$': LogSvParams(theta=1.0, kappa1=4.0, kappa2=0.0, beta=0.0, volvol=VOLVOL), 26 | '$(\kappa_{1}=4, \kappa_{2}=4)$': LogSvParams(theta=1.0, kappa1=4.0, kappa2=4.0, beta=0.0, volvol=VOLVOL), 27 | '$(\kappa_{1}=4, \kappa_{2}=8)$': LogSvParams(theta=1.0, kappa1=4.0, kappa2=8.0, beta=0.0, volvol=VOLVOL)} 28 | 29 | SS_PARAMS = {'$\kappa_{1}=1$': LogSvParams(theta=1.0, kappa1=1.0, kappa2=0.0, beta=0.0, volvol=VOLVOL), 30 | '$\kappa_{1}=4$': LogSvParams(theta=1.0, kappa1=4.0, kappa2=4.0, beta=0.0, volvol=VOLVOL), 31 | '$\kappa_{1}=8$': LogSvParams(theta=1.0, kappa1=8.0, kappa2=8.0, beta=0.0, volvol=VOLVOL)} 32 | 33 | 34 | @njit(cache=False, fastmath=True) 35 | def integral_x_over_sigma(x_grid: np.ndarray, 36 | sigma: np.ndarray, 37 | g_sigma: np.ndarray 38 | ) -> np.ndarray: 39 | pdf = np.zeros_like(x_grid) 40 | sigma_inv = np.reciprocal(sigma) 41 | den = (1.0 / np.sqrt(2.0*np.pi)) * sigma_inv 42 | for idx, x_ in enumerate(x_grid): 43 | pdf[idx] = np.nansum(den * np.exp(-0.5*(x_*x_)*sigma_inv) * g_sigma) 44 | return pdf 45 | 46 | 47 | def vol_moment(params: LogSvParams, r: int = 1): 48 | nu = 2.0 * (params.kappa2 * params.theta - params.kappa1) / params.vartheta2 - 1.0 49 | q = 2.0 * params.kappa1 * params.theta / params.vartheta2 50 | b = 2.0 * params.kappa2 / params.vartheta2 51 | y = np.power(b/q, r/2.0) * sps.kv(nu+r, 2.0*np.sqrt(q*b)) / sps.kv(nu, 2.0*np.sqrt(q*b)) 52 | return y 53 | 54 | 55 | def vol_skeweness(params: LogSvParams): 56 | m3_r = vol_moment(params=params, r=3) 57 | m2_r = vol_moment(params=params, r=2) 58 | m1_r = vol_moment(params=params, r=1) 59 | m2 = m2_r - m1_r * m1_r 60 | # y = (m3_r-3*m1_r*m2_r-m1_r*m1_r*m1_r)/np.power(m2_r, 1.5) 61 | y = (m3_r-3*m1_r*m2-m1_r*m1_r*m1_r)/np.power(m2, 1.5) 62 | # y = (m3_r - 3.0 * m1_r * m2_r - 2.0*m1_r * m1_r * m1_r) / np.power(m2, 1.5) 63 | # print(f"m1_r={m1_r}, m2_r={m2_r}, m2={m2}, m3_r={m3_r}, skew={y}") 64 | return y 65 | 66 | 67 | def steady_state(sigma: np.ndarray, 68 | params: LogSvParams 69 | ) -> np.ndarray: 70 | nu = 2.0*(params.kappa2*params.theta-params.kappa1)/params.vartheta2 - 1.0 71 | q = 2.0*params.kappa1*params.theta/params.vartheta2 72 | b = 2.0*params.kappa2/params.vartheta2 73 | if params.kappa1 >= 1e-6: 74 | if params.kappa2 >= 1e-6: 75 | c = np.power(b/q, nu/2.0) / (2.0*sps.kv(nu, 2.0*np.sqrt(q*b))) 76 | else: 77 | c = np.power(q, -nu) / sps.gamma(-nu) 78 | else: 79 | raise NotImplementedError(f"kappa1 = 0 is not implemented") 80 | g = c*np.power(sigma, nu-1.0)*np.exp(-q*np.reciprocal(sigma)-b*sigma) 81 | return g 82 | 83 | 84 | def plot_steady_state(params_dict: Dict[str, LogSvParams] = SS_PDF_PARAMS, 85 | title: str = None, 86 | ax: plt.Subplot = None 87 | ) -> None: 88 | 89 | sigma = np.linspace(1e-4, 4.0, 1000) 90 | qs = [] 91 | for key, params in params_dict.items(): 92 | qs.append(pd.Series(steady_state(sigma=sigma, params=params), index=sigma, name=key)) 93 | ss_pdf = pd.concat(qs, axis=1) 94 | 95 | sns.lineplot(data=ss_pdf, dashes=False, ax=ax) 96 | ax.set_xlim(left=0.0) 97 | ax.set_ylim(bottom=0.0) 98 | ax.set_title(title, color='darkblue') 99 | ax.set_xlabel('$\sigma$', fontsize=12) 100 | yvar_format = '{:.2f}' 101 | ax.xaxis.set_major_formatter(mticker.FuncFormatter(lambda z, _: yvar_format.format(z))) 102 | 103 | 104 | def plot_steady_state_x(params_dict: Dict[str, LogSvParams] = SS_PDF_PARAMS, 105 | title: str = None, 106 | ax: plt.Subplot = None 107 | ) -> None: 108 | 109 | sigma = np.linspace(1e-4, 5.0, 1000) 110 | x = np.linspace(-5.0, 5.0, 200) 111 | qs = [] 112 | for key, params in params_dict.items(): 113 | g_sigma = steady_state(sigma=sigma, params=params) 114 | x_pdf = integral_x_over_sigma(x_grid=x, sigma=sigma, g_sigma=g_sigma) 115 | qs.append(pd.Series(x_pdf, index=x, name=key)) 116 | 117 | ss_pdf = pd.concat(qs, axis=1) 118 | 119 | if ax is None: 120 | with sns.axes_style("darkgrid"): 121 | fig, ax = plt.subplots(1, 1, figsize=(10, 4.0), tight_layout=True) 122 | sns.lineplot(data=ss_pdf, ax=ax) 123 | #ax.set_xlim(left=0.0) 124 | ax.set_ylim(bottom=0.0) 125 | ax.set_title(title, color='darkblue') 126 | 127 | 128 | def plot_vol_skew(params_dict=SS_PARAMS, 129 | title: str = f'Skeweness of volatility as function of $\kappa_{2}$', 130 | ax: plt.Subplot = None 131 | ) -> None: 132 | 133 | kappa2s = np.linspace(0.5, 10.0, 100) 134 | qs = [] 135 | 136 | def skewness(params: LogSvParams) -> np.ndarray: 137 | skew = np.zeros_like(kappa2s) 138 | for idx, kappa2 in enumerate(kappa2s): 139 | params.kappa2 = kappa2 140 | m3_r = vol_moment(params=params, r=3) 141 | m2_r = vol_moment(params=params, r=2) 142 | m1_r = vol_moment(params=params, r=1) 143 | m2 = m2_r - m1_r * m1_r 144 | # y = (m3_r-3*m1_r*m2_r-m1_r*m1_r*m1_r)/np.power(m2_r, 1.5) 145 | skew[idx] = (m3_r - 3 * m1_r * m2 - m1_r * m1_r * m1_r) / np.power(m2, 1.5) 146 | return skew 147 | 148 | for key, params in params_dict.items(): 149 | qs.append(pd.Series(skewness(params=params), index=kappa2s, name=key)) 150 | 151 | ss_pdf = pd.concat(qs, axis=1) 152 | 153 | sns.lineplot(data=ss_pdf, dashes=False, ax=ax) 154 | ax.set_xlabel(f'$\kappa_{2}$') 155 | if title is not None: 156 | ax.set_title(title, fontsize=12, color='darkblue') 157 | 158 | 159 | def plot_ss_kurtosis(params_dict=SS_PARAMS, 160 | title: str = f'Excess kurtosis of log-returns as function of $\kappa_{2}$', 161 | ax: plt.Subplot = None 162 | ) -> None: 163 | 164 | kappa2s = np.linspace(0.5, 10.0, 100) 165 | qs = [] 166 | 167 | def kurtosys(params: LogSvParams) -> np.ndarray: 168 | kurt = np.zeros_like(kappa2s) 169 | for idx, kappa2 in enumerate(kappa2s): 170 | nu = 2.0 * (kappa2 * params.theta-params.kappa1) / params.vartheta2 - 1.0 171 | q = 2.0 * params.kappa1 * params.theta / params.vartheta2 172 | b = 2.0 * kappa2 / params.vartheta2 173 | arg = 2.0*np.sqrt(q*b) 174 | kurt[idx] = 3.0*sps.kv(nu+4.0, arg)*sps.kv(nu, arg)/np.square(sps.kv(nu+2, arg)) - 3.0 175 | return kurt 176 | 177 | for key, params in params_dict.items(): 178 | qs.append(pd.Series(kurtosys(params=params), index=kappa2s, name=key)) 179 | 180 | ss_pdf = pd.concat(qs, axis=1) 181 | 182 | sns.lineplot(data=ss_pdf, dashes=False, ax=ax) 183 | ax.set_xlabel(f'$\kappa_{2}$') 184 | if title is not None: 185 | ax.set_title(title, fontsize=12, color='darkblue') 186 | 187 | 188 | class UnitTests(Enum): 189 | PLOT_VOL_STEADY_STATE = 1 190 | PLOT_SS_PDF = 2 191 | PLOT_X_PDF = 3 192 | PLOT_KURT = 4 193 | JOINT_FIGURE = 5 194 | SKEWENESS = 6 195 | 196 | 197 | def run_unit_test(unit_test: UnitTests): 198 | 199 | if unit_test == UnitTests.PLOT_VOL_STEADY_STATE: 200 | with sns.axes_style("darkgrid"): 201 | fig, ax = plt.subplots(1, 1, figsize=(18, 10), tight_layout=True) 202 | plot_steady_state(title='Steady state distribution of volatility with $\kappa_{1}=4$', 203 | ax=ax) 204 | 205 | elif unit_test == UnitTests.PLOT_SS_PDF: 206 | with sns.axes_style("darkgrid"): 207 | fig, ax = plt.subplots(1, 1, figsize=(12, 6), tight_layout=True) 208 | plot_steady_state(ax=ax) 209 | 210 | elif unit_test == UnitTests.PLOT_X_PDF: 211 | with sns.axes_style("darkgrid"): 212 | fig, ax = plt.subplots(1, 1, figsize=(12, 6), tight_layout=True) 213 | plot_steady_state_x(ax=ax) 214 | 215 | elif unit_test == UnitTests.PLOT_KURT: 216 | with sns.axes_style("darkgrid"): 217 | fig, ax = plt.subplots(1, 1, figsize=(6, 6), tight_layout=True) 218 | plot_ss_kurtosis(ax=ax) 219 | 220 | elif unit_test == UnitTests.SKEWENESS: 221 | with sns.axes_style("darkgrid"): 222 | fig, ax = plt.subplots(1, 1, figsize=(6, 6), tight_layout=True) 223 | plot_vol_skew(ax=ax) 224 | 225 | elif unit_test == UnitTests.JOINT_FIGURE: 226 | with sns.axes_style("darkgrid"): 227 | fig, axs = plt.subplots(1, 3, figsize=(18, 6), tight_layout=True) 228 | plot_steady_state(title='(A) Steady state distribution of the volatility', 229 | ax=axs[0]) 230 | plot_vol_skew(title=f'(B) Skeweness of volatility as function of $\kappa_{2}$', 231 | ax=axs[1]) 232 | plot_ss_kurtosis(title=f'(C) Excess kurtosis of log-returns as function of $\kappa_{2}$', 233 | ax=axs[2]) 234 | 235 | is_save = True 236 | if is_save: 237 | plot.save_fig(fig=fig, local_path='../../docs/figures//', file_name='vol_steady_state') 238 | 239 | plt.show() 240 | 241 | 242 | if __name__ == '__main__': 243 | 244 | unit_test = UnitTests.PLOT_VOL_STEADY_STATE 245 | 246 | is_run_all_tests = False 247 | if is_run_all_tests: 248 | for unit_test in UnitTests: 249 | run_unit_test(unit_test=unit_test) 250 | else: 251 | run_unit_test(unit_test=unit_test) 252 | -------------------------------------------------------------------------------- /my_papers/logsv_model_wtih_quadratic_drift/vol_drift.py: -------------------------------------------------------------------------------- 1 | """ 2 | figure 4 in the logsv_model_wtih_quadratic_drift 3 | plot volatility drift 4 | """ 5 | import pandas as pd 6 | import numpy as np 7 | import matplotlib.pyplot as plt 8 | import matplotlib.ticker as mticker 9 | import seaborn as sns 10 | from typing import Dict, List 11 | from enum import Enum 12 | 13 | import stochvolmodels.utils.plots as plot 14 | from stochvolmodels import LogSvParams 15 | 16 | VOLVOL = 1.75 17 | 18 | 19 | DRIFT_PARAMS = {'$(\kappa_{1}=4, \kappa_{2}=0)$': LogSvParams(sigma0=1.0, theta=1.0, kappa1=4.0, kappa2=0.0, beta=0.0, volvol=VOLVOL), 20 | '$(\kappa_{1}=4, \kappa_{2}=4)$': LogSvParams(sigma0=1.0, theta=1.0, kappa1=4.0, kappa2=4.0, beta=0.0, volvol=VOLVOL), 21 | '$(\kappa_{1}=4, \kappa_{2}=8)$': LogSvParams(sigma0=1.0, theta=1.0, kappa1=4.0, kappa2=8.0, beta=0.0, volvol=VOLVOL)} 22 | 23 | 24 | def plot_drift(params: Dict[str, LogSvParams] = DRIFT_PARAMS, 25 | axs: List[plt.Subplot] = None 26 | ) -> None: 27 | x = np.linspace(0.0, 2.0, 200) 28 | 29 | drifts = [] 30 | drifts_delta = [] 31 | for key, param in params.items(): 32 | drift1 = param.kappa1*(param.theta - x) 33 | drift2 = param.kappa1 * param.theta - (param.kappa1 - param.kappa2 * param.theta) * x - param.kappa2 * x * x 34 | drifts.append(pd.Series(drift2, index=x, name=key)) 35 | drifts_delta.append(pd.Series(drift2-drift1, index=x, name=key)) 36 | drifts = pd.concat(drifts, axis=1) / 260.0 37 | drifts_delta = pd.concat(drifts_delta, axis=1) / 260.0 38 | 39 | dfs = {'(A) Volatility drift per day as function of $\sigma_{t}$': drifts, 40 | '(B) Volatility drift relative to the linear drift': drifts_delta} 41 | 42 | for idx, (key, df) in enumerate(dfs.items()): 43 | ax = axs[idx] 44 | sns.lineplot(data=df, dashes=False, ax=ax) 45 | yvar_format = '{:.2f}' 46 | ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda z, _: yvar_format.format(z))) 47 | ax.set_title(key, fontsize=12, color='darkblue') 48 | ax.set_xlabel('$\sigma_{t}$', fontsize=12) 49 | ax.set_xlim((0.0, None)) 50 | plot.align_y_limits_axs(axs=axs) 51 | 52 | 53 | class UnitTests(Enum): 54 | PLOT_DRIFT = 1 55 | 56 | 57 | def run_unit_test(unit_test: UnitTests): 58 | 59 | if unit_test == UnitTests.PLOT_DRIFT: 60 | with sns.axes_style('darkgrid'): 61 | fig, axs = plt.subplots(1, 2, figsize=(18, 6), tight_layout=True) 62 | plot_drift(axs=axs) 63 | 64 | is_save = False 65 | if is_save: 66 | plot.save_fig(fig=fig, 67 | local_path='../../docs/figures//', 68 | file_name='vol_drift') 69 | 70 | plt.show() 71 | 72 | 73 | if __name__ == '__main__': 74 | 75 | unit_test = UnitTests.PLOT_DRIFT 76 | 77 | is_run_all_tests = False 78 | if is_run_all_tests: 79 | for unit_test in UnitTests: 80 | run_unit_test(unit_test=unit_test) 81 | else: 82 | run_unit_test(unit_test=unit_test) 83 | -------------------------------------------------------------------------------- /my_papers/risk_premia_gmm/check_kernel.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | import pandas as pd 4 | import qis as qis 5 | import matplotlib.pyplot as plt 6 | 7 | 8 | def plot_kernels(kappa: float = -2.0): 9 | x = np.linspace(-0.5, 1.0, 1000) 10 | exp_k = pd.Series(np.exp(x*kappa), index=x, name='Exp') 11 | # linear_k = pd.Series((np.power(1.0+x, 1-kappa)-1.0)/(1-kappa), index=x, name='Linear') 12 | linear_k = pd.Series(1.0 + x*kappa + 0.5*np.square(x*kappa) + (1.0/6.0)*np.square(x*kappa)*x*kappa, index=x, name='Linear') 13 | df = pd.concat([exp_k, linear_k], axis=1) 14 | qis.plot_line(df=df) 15 | 16 | 17 | plot_kernels() 18 | 19 | plt.show() 20 | 21 | -------------------------------------------------------------------------------- /my_papers/risk_premia_gmm/plot_gmm.py: -------------------------------------------------------------------------------- 1 | """ 2 | illustrations of gmm vols 3 | """ 4 | import numpy as np 5 | import pandas as pd 6 | import qis as qis 7 | import seaborn as sns 8 | import matplotlib.pyplot as plt 9 | from typing import List 10 | from stochvolmodels import GmmParams, OptionChain, GmmPricer 11 | 12 | 13 | def plot_gmm_pdfs(params: GmmParams, 14 | option_chain0: OptionChain, 15 | nstdev: float = 10.0, 16 | titles: List[str] = None, 17 | axs: List[plt.Subplot] = None 18 | ) -> plt.Figure: 19 | """ 20 | plot gmm pdf and model fit 21 | """ 22 | stdev = nstdev * params.get_get_avg_vol() * np.sqrt(params.ttm) 23 | x = np.linspace(-stdev, stdev, 3000) 24 | state_pdfs, agg_pdf = params.compute_state_pdfs(x=x) 25 | 26 | columns = [] 27 | for idx in range(len(params.gmm_weights)): 28 | columns.append( 29 | f"state-{idx + 1}: mean={params.gmm_mus[idx]:0.2f}, vol={params.gmm_vols[idx]:0.2f}, weight={params.gmm_weights[idx]:0.2f}") 30 | 31 | state_pdfs = pd.DataFrame(state_pdfs, index=x, columns=columns) 32 | agg_pdf = pd.Series(agg_pdf, index=x, name='Aggregate PDF') 33 | df = pd.concat([agg_pdf, state_pdfs], axis=1) 34 | 35 | kwargs = dict(fontsize=14, framealpha=0.80) 36 | 37 | if axs is None: 38 | with sns.axes_style("darkgrid"): 39 | fig, axs = plt.subplots(1, 2, figsize=(16, 4.5)) 40 | else: 41 | fig = None 42 | 43 | qis.plot_line(df=df, 44 | linestyles=['--'] + ['-'] * len(params.gmm_weights), 45 | y_limits=(0.0, None), 46 | xvar_format='{:,.2f}', 47 | xlabel='log-price', 48 | first_color_fixed=True, 49 | ax=axs[0], 50 | **kwargs) 51 | axs[0].get_lines()[0].set_linewidth(4.0) 52 | axs[0].get_legend().get_lines()[0].set_linewidth(4.0) 53 | qis.set_title(ax=axs[0], title='(A) State PDF and Aggregate Risk-Neutral PDF', **kwargs) 54 | 55 | gmm_pricer = GmmPricer() 56 | gmm_pricer.plot_model_ivols_vs_bid_ask(option_chain=option_chain0, params=params, 57 | is_log_strike_xaxis=True, 58 | axs=[axs[1]], 59 | **kwargs) 60 | return fig 61 | -------------------------------------------------------------------------------- /my_papers/risk_premia_gmm/q_kernel.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | from numba import njit 4 | from stochvolmodels import npdf, infer_bsm_implied_vol 5 | 6 | 7 | @njit 8 | def compute_normal_pdf(x: np.ndarray): 9 | dx = x[1] - x[0] 10 | return dx*npdf(x) 11 | 12 | 13 | @njit 14 | def value_under_q_kernel(b: float, pdf: np.ndarray, x: np.ndarray, payoff: np.ndarray, forward: float = 1.0): 15 | c = -0.5 + (2.0*b+1.0)*np.log(forward) 16 | norm = np.exp(0.5*np.square(c)/(2.0*b+1))/np.sqrt(2.0*b+1) 17 | q_payoff = np.sum(pdf*np.exp(c*x-b*np.square(x))*payoff) / norm 18 | return q_payoff 19 | 20 | 21 | @njit 22 | def value_payoff(pdf: np.ndarray, payoff: np.ndarray): 23 | return np.sum(pdf*payoff) 24 | 25 | 26 | x = np.linspace(-5.0, 5.0, 20000) 27 | pdf = compute_normal_pdf(x) 28 | 29 | print(f"sum={np.sum(pdf)}, mean={np.sum(x * pdf)}, std={np.sqrt(np.sum(np.square(x) * pdf) - np.square(np.sum(x * pdf)))}") 30 | 31 | payoff = np.exp(x) 32 | q_payoff = value_under_q_kernel(b=2.0, pdf=pdf, x=x, payoff=payoff, forward=1) 33 | print(f"q_payoff={q_payoff}") 34 | 35 | strikes = np.linspace(0.2, 2.0, 21) 36 | 37 | values, model_vols = np.zeros_like(strikes), np.zeros_like(strikes) 38 | values_q, model_vols_q = np.zeros_like(strikes), np.zeros_like(strikes) 39 | for idx, strike in enumerate(strikes): 40 | spot = np.exp(x-0.5) 41 | payoff = np.maximum(spot-strike, 0.0) 42 | model_price = value_payoff(pdf=pdf, payoff=payoff) 43 | values[idx] = model_price 44 | model_vols[idx] = infer_bsm_implied_vol(forward=1.0, ttm=1.0, given_price=model_price, strike=strike, optiontype='C') 45 | payoff = np.maximum(np.exp(x)-strike, 0.0) 46 | model_price_q = value_under_q_kernel(b=0.25, pdf=pdf, x=x, payoff=payoff) 47 | values_q[idx] = model_price_q 48 | model_vols_q[idx] = infer_bsm_implied_vol(forward=1.0, ttm=1.0, given_price=model_price_q, strike=strike, optiontype='C') 49 | 50 | print(f"values={values}") 51 | print(f"values_q={values_q}") 52 | print(f"model_vols={model_vols}") 53 | print(f"model_vols_q={model_vols_q}") 54 | -------------------------------------------------------------------------------- /my_papers/risk_premia_gmm/run_gmm_fit.py: -------------------------------------------------------------------------------- 1 | """ 2 | example of fitting GMM 3 | see StochVolModels/examples/run_gmm_fit.py 4 | """ 5 | import matplotlib.pyplot as plt 6 | import seaborn as sns 7 | import qis as qis 8 | from stochvolmodels import (get_btc_test_chain_data, 9 | get_spy_test_chain_data, 10 | OptionChain, GmmPricer) 11 | from my_papers.risk_premia_gmm.plot_gmm import plot_gmm_pdfs 12 | 13 | # get test option chain data 14 | # option_chain = get_btc_test_chain_data() 15 | option_chain = get_spy_test_chain_data() 16 | 17 | # run GMM fit 18 | gmm_pricer = GmmPricer() 19 | fit_params = gmm_pricer.calibrate_model_params_to_chain(option_chain=option_chain, n_mixtures=4) 20 | 21 | # illustrate fitted parameters and model fit to market bid-ask 22 | # plot two ids 23 | ids = ['2m', '6m'] 24 | n = len(ids) 25 | with sns.axes_style('darkgrid'): 26 | fig, axs = plt.subplots(n, 2, figsize=(14, 12), tight_layout=True) 27 | # axs = qis.to_flat_list(axs) 28 | current_ax = 0 29 | 30 | for key, params in fit_params.items(): 31 | print(f"{key}: {params}") 32 | if key in ids: 33 | option_chain0 = OptionChain.get_slices_as_chain(option_chain, ids=[key]) 34 | # gmm_pricer.plot_model_ivols_vs_bid_ask(option_chain=option_chain0, params=params, axs=[axs[idx]]) 35 | plot_gmm_pdfs(params=params, option_chain0=option_chain0, axs=axs[current_ax, :]) 36 | qis.set_title(ax=axs[current_ax, 0], title=f"{key}-slice: (A) State PDF and Aggregate Risk-Neutral PDF") 37 | qis.set_title(ax=axs[current_ax, 1], title=f"{key}-slice: Model to Market Bid/Ask vols") 38 | current_ax += 1 39 | 40 | qis.set_suptitle(fig, title='Fit of 4-state GMM to SPY implied vols @ 15_Jul_2022_10_23_09') 41 | plt.show() 42 | -------------------------------------------------------------------------------- /my_papers/sv_for_factor_hjm/README.md: -------------------------------------------------------------------------------- 1 | This module contains python code for the analysis and figures for paper 2 | [Stochastic Volatility for Factor Heath-Jarrow-Morton Framework](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4646925) 3 | by Artur Sepp and Parviz Rakhmonov 4 | 5 | 6 | 7 | See the description of data and analysis in the paper. 8 | 9 | Figures in the paper are generated using unittests in 10 | ```python 11 | calibration_fig_5_6_7.py 12 | calibration_fig_8_9.py 13 | ``` 14 | https://github.com/ArturSepp/StochVolModels/tree/main/my_papers/sv_for_factor_hjm 15 | 16 | See the description of data and analysis in the paper. -------------------------------------------------------------------------------- /my_papers/t_distribution/illustrations.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | import seaborn as sns 5 | from typing import List 6 | from enum import Enum 7 | import qis as qis 8 | 9 | from stochvolmodels import (compute_vanilla_price_tdist, 10 | infer_bsm_ivols_from_slice_prices, 11 | infer_normal_ivols_from_slice_prices) 12 | 13 | from stochvolmodels.pricers.analytic.tdist import (imply_drift_tdist, 14 | compute_default_prob_tdist, 15 | compute_forward_tdist) 16 | 17 | 18 | def plot_implied_drift_forward_defaultp(spot: float = 1.0, vol: float = 0.2, nu: float = 3.0 ) -> plt.Figure: 19 | ttms = np.linspace(0.004, 1.0, 20) 20 | rf_rates = np.linspace(0.0, 0.05, 6) 21 | mus_ttm = {} 22 | forwards_ttm = {} 23 | default_prob_ttm = {} 24 | for rf_rate in rf_rates: 25 | mus = np.zeros_like(ttms) 26 | forwards = np.zeros_like(ttms) 27 | default_prob = np.zeros_like(ttms) 28 | for idx, ttm in enumerate(ttms): 29 | mus[idx] = imply_drift_tdist(rf_rate=rf_rate, vol=vol, nu=nu, ttm=ttm) 30 | forwards[idx] = compute_forward_tdist(spot=spot, rf_rate=rf_rate, vol=vol, nu=nu, ttm=ttm) 31 | default_prob[idx] = compute_default_prob_tdist(rf_rate=rf_rate, vol=vol, nu=nu, ttm=ttm) 32 | mus_ttm[f"rf_rate={rf_rate:,.2%}"] = pd.Series(mus, index=ttms) 33 | forwards_ttm[f"rf_rate={rf_rate:,.2%}"] = pd.Series(forwards, index=ttms) 34 | default_prob_ttm[f"rf_rate={rf_rate:,.2%}"] = pd.Series(default_prob, index=ttms) 35 | 36 | mus_ttm = pd.DataFrame.from_dict(mus_ttm, orient='columns') 37 | forwards_ttm = pd.DataFrame.from_dict(forwards_ttm, orient='columns') 38 | default_prob_ttm = pd.DataFrame.from_dict(default_prob_ttm, orient='columns') 39 | 40 | with sns.axes_style("darkgrid"): 41 | fig, axs = plt.subplots(3, 1, figsize=(14, 14), tight_layout=True) 42 | qis.plot_line(df=mus_ttm, 43 | yvar_format='{:,.2%}', 44 | xvar_format='{:,.2f}', 45 | xlabel='ttm', 46 | title='(A) Implied Drift', 47 | ax=axs[0]) 48 | axs[0].set_xticklabels('') 49 | qis.plot_line(df=forwards_ttm, 50 | yvar_format='{:,.2f}', 51 | xvar_format='{:,.2f}', 52 | xlabel='ttm', 53 | title='(B) Model Forward', 54 | ax=axs[1]) 55 | axs[1].set_xticklabels('') 56 | qis.plot_line(df=default_prob_ttm, 57 | yvar_format='{:,.2%}', 58 | xvar_format='{:,.2f}', 59 | xlabel='ttm', 60 | title='(C) Model Default prob', 61 | ax=axs[2]) 62 | 63 | return fig 64 | 65 | 66 | def plot_tdist_ivols_vs_bsm_normal(spot: float = 1.0, 67 | vol: float = 0.5, 68 | nu: float = 2.5, 69 | ttm: float = 1.0 / 12.0, 70 | rf_rate: float = 0.0, 71 | ax: plt.Subplot = None 72 | ) -> None: 73 | strikes = np.linspace(0.5, 1.5, 40) 74 | optiontypes_ttm = np.where(strikes <= 1.0, 'P', 'C') 75 | prices = compute_vanilla_price_tdist(spot=1.0, strikes=strikes, optiontypes=optiontypes_ttm, ttm=ttm, vol=vol, nu=nu, rf_rate=rf_rate) 76 | discfactor = np.exp(-rf_rate*ttm) 77 | forward = np.exp(rf_rate*ttm) * spot 78 | bsm_vols = infer_bsm_ivols_from_slice_prices(ttm=ttm, forward=forward, strikes=strikes, 79 | optiontypes=optiontypes_ttm, 80 | model_prices=prices, 81 | discfactor=discfactor) 82 | bsm_vols = pd.Series(bsm_vols, index=strikes, name='BSM implied vol') 83 | normal_vols = infer_normal_ivols_from_slice_prices(ttm=ttm, forward=forward, strikes=strikes, 84 | optiontypes=optiontypes_ttm, 85 | model_prices=prices, 86 | discfactor=discfactor) 87 | normal_vols = pd.Series(normal_vols, index=strikes, name='Normal implied vol') 88 | vols = pd.concat([bsm_vols, normal_vols], axis=1) 89 | qis.plot_line(df=vols, title=f"t-distribution implied vols, nu = {nu:0.2f}", ax=ax) 90 | 91 | 92 | def plot_tdist_ivols_nu(spot: float = 1.0, 93 | vol: float = 0.5, 94 | ttm: float= 1.0 / 12.0, 95 | nus: List[float] = [2.5, 3.0, 4.0, 5.0, 10.0, 20.0], 96 | rf_rate: float = 0.00, 97 | ax: plt.Subplot = None 98 | ) -> None: 99 | 100 | forward = spot * np.exp(ttm*rf_rate) 101 | discfactor = np.exp(-ttm*rf_rate) 102 | strikes = np.linspace(0.5, 1.5, 100) 103 | optiontypes_ttm = np.where(strikes <= 1.0, 'P', 'C') 104 | bsm_vols = {} 105 | for nu in nus: 106 | prices = compute_vanilla_price_tdist(spot=spot, strikes=strikes, optiontypes=optiontypes_ttm, ttm=ttm, vol=vol, nu=nu, 107 | rf_rate=rf_rate) 108 | print(prices) 109 | bsm_vols[f"nu={nu:0.2f}"] = infer_bsm_ivols_from_slice_prices(ttm=ttm, forward=forward, discfactor=discfactor, 110 | strikes=strikes, 111 | optiontypes=optiontypes_ttm, 112 | model_prices=prices) 113 | bsm_vols = pd.DataFrame.from_dict(bsm_vols, orient='columns') 114 | bsm_vols.index = strikes 115 | qis.plot_line(df=bsm_vols, 116 | title=f"t-distribution implied BSM vols, ttm={ttm:0.2f}", 117 | xvar_format='{:,.0%}', 118 | yvar_format='{:,.0%}', 119 | xlabel='% strike', 120 | ylabel='Implied vol', 121 | ax=ax) 122 | 123 | 124 | def plot_tdist_ivols_vol(vols: List[float] = [0.2, 0.3, 0.4, 0.8], 125 | ttm: float= 1.0 / 12.0, 126 | nu: float = 2.5, 127 | ax: plt.Subplot = None 128 | ) -> None: 129 | 130 | strikes = np.linspace(0.5, 1.5, 100) 131 | optiontypes_ttm = np.where(strikes <= 1.0, 'P', 'C') 132 | bsm_vols = {} 133 | for vol in vols: 134 | prices = compute_vanilla_price_tdist(spot=1.0, strikes=strikes, optiontypes=optiontypes_ttm, ttm=ttm, vol=vol, nu=nu) 135 | bsm_vols[f"vol={vol:0.2f}"] = infer_bsm_ivols_from_slice_prices(ttm=ttm, forward=1.0, discfactor=1.0, strikes=strikes, 136 | optiontypes=optiontypes_ttm, 137 | model_prices=prices) 138 | bsm_vols = pd.DataFrame.from_dict(bsm_vols, orient='columns') 139 | bsm_vols.index = strikes 140 | qis.plot_line(df=bsm_vols, 141 | title=f"t-distribution mplied BSM vols, ttm={ttm:0.2f}", 142 | xvar_format='{:,.0%}', 143 | yvar_format='{:,.0%}', 144 | xlabel='% strike', 145 | ylabel='Implied vol', 146 | ax=ax) 147 | 148 | 149 | class UnitTests(Enum): 150 | PLOT_IMPLIED_DRIFT_FORWARD_DEFAULTPROB = 1 151 | PLOT_IMPLIED_VOLS_VS_BSM_NORMAL = 2 152 | PLOT_IVOLS_NU = 3 153 | PLOT_IVOLS_VOL = 4 154 | 155 | 156 | def run_unit_test(unit_test: UnitTests): 157 | 158 | local_path = 'C://Users//artur//OneDrive//My Papers//Working Papers//Old Papers//Conditional Volatility Models. Zurich. May 2021//Figures//' 159 | 160 | if unit_test == UnitTests.PLOT_IMPLIED_DRIFT_FORWARD_DEFAULTPROB: 161 | fig = plot_implied_drift_forward_defaultp() 162 | qis.save_fig(fig=fig, file_name='mus', local_path=local_path) 163 | 164 | elif unit_test == UnitTests.PLOT_IMPLIED_VOLS_VS_BSM_NORMAL: 165 | plot_tdist_ivols_vs_bsm_normal(vol=0.5, nu=2.5) 166 | plot_tdist_ivols_vs_bsm_normal(vol=0.5, nu=5.0) 167 | 168 | elif unit_test == UnitTests.PLOT_IVOLS_NU: 169 | rf_rate = 0.0 170 | with sns.axes_style('darkgrid'): 171 | fig, axs = plt.subplots(1, 2, figsize=(14, 6), tight_layout=True) 172 | plot_tdist_ivols_nu(vol=0.2, ttm=5.0/252.0, rf_rate=rf_rate, ax=axs[0]) 173 | plot_tdist_ivols_nu(vol=0.2, ttm=1.0/12.0, rf_rate=rf_rate, ax=axs[1]) 174 | qis.align_y_limits_axs(axs) 175 | qis.save_fig(fig=fig, file_name='vols_in_nu', local_path=local_path) 176 | 177 | elif unit_test == UnitTests.PLOT_IVOLS_VOL: 178 | with sns.axes_style('darkgrid'): 179 | fig, axs = plt.subplots(1, 2, figsize=(14, 6), tight_layout=True) 180 | plot_tdist_ivols_vol(nu=2.5, ttm=5.0/252.0, ax=axs[0]) 181 | plot_tdist_ivols_vol(nu=2.5, ttm=1.0/12.0, ax=axs[1]) 182 | qis.align_y_limits_axs(axs) 183 | qis.save_fig(fig=fig, file_name='vols_in_vol', local_path=local_path) 184 | 185 | plt.show() 186 | 187 | 188 | if __name__ == '__main__': 189 | 190 | unit_test = UnitTests.PLOT_IVOLS_NU 191 | 192 | is_run_all_tests = False 193 | if is_run_all_tests: 194 | for unit_test in UnitTests: 195 | run_unit_test(unit_test=unit_test) 196 | else: 197 | run_unit_test(unit_test=unit_test) 198 | -------------------------------------------------------------------------------- /my_papers/t_distribution/market_data_fit.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | import pandas as pd 4 | import matplotlib.pyplot as plt 5 | import seaborn as sns 6 | import qis as qis 7 | from scipy.special import betainc, gamma 8 | from scipy.optimize import fsolve 9 | from typing import Union 10 | from enum import Enum 11 | 12 | from stochvolmodels import TdistPricer, OptionChain 13 | 14 | 15 | class UnitTests(Enum): 16 | SPY_FIT = 1 17 | GOLD_FIT = 2 18 | BTC_FIT = 3 19 | 20 | 21 | def run_unit_test(unit_test: UnitTests): 22 | 23 | import seaborn as sns 24 | import qis as qis 25 | import stochvolmodels.data.test_option_chain as chains 26 | 27 | local_path = 'C://Users//artur//OneDrive//My Papers//Working Papers//Old Papers//Conditional Volatility Models. Zurich. May 2021//Figures//' 28 | 29 | if unit_test == UnitTests.SPY_FIT: 30 | option_chain = chains.get_spy_test_chain_data() 31 | elif unit_test == UnitTests.GOLD_FIT: 32 | option_chain = chains.get_gld_test_chain_data() 33 | elif unit_test == UnitTests.BTC_FIT: 34 | option_chain = chains.get_btc_test_chain_data() 35 | 36 | else: 37 | raise NotImplementedError 38 | 39 | tdist_pricer = TdistPricer() 40 | fit_params = tdist_pricer.calibrate_model_params_to_chain(option_chain=option_chain) 41 | 42 | with sns.axes_style('darkgrid'): 43 | fig, axs = plt.subplots(2, 2, figsize=(14, 12), tight_layout=True) 44 | axs = qis.to_flat_list(axs) 45 | 46 | for idx, (key, params) in enumerate(fit_params.items()): 47 | print(f"{key}: {params}") 48 | title = f"maturity-{key}: nu={params.nu:0.2f}, vol={params.vol:0.2f}, drift={params.drift:0.2%}" 49 | option_chain0 = OptionChain.get_slices_as_chain(option_chain, ids=[key]) 50 | tdist_pricer.plot_model_ivols_vs_bid_ask(option_chain=option_chain0, params=params, 51 | title=title, axs=[axs[idx]]) 52 | 53 | qis.save_fig(fig, file_name=f"{unit_test.name.lower()}", local_path=local_path) 54 | plt.show() 55 | 56 | 57 | if __name__ == '__main__': 58 | 59 | unit_test = UnitTests.GOLD_FIT 60 | 61 | is_run_all_tests = False 62 | if is_run_all_tests: 63 | for unit_test in UnitTests: 64 | run_unit_test(unit_test=unit_test) 65 | else: 66 | run_unit_test(unit_test=unit_test) 67 | -------------------------------------------------------------------------------- /my_papers/t_distribution/mc_pricer_with_kernel.py: -------------------------------------------------------------------------------- 1 | """ 2 | check mc pricer with kernel 3 | """ 4 | import numpy as np 5 | import pandas as pd 6 | import matplotlib.pyplot as plt 7 | import qis as qis 8 | from numba import njit 9 | from scipy.special import betainc, gamma 10 | from scipy.optimize import fsolve 11 | from typing import Union 12 | from enum import Enum 13 | 14 | from stochvolmodels.pricers.analytic.tdist import compute_upsilon 15 | from stochvolmodels import infer_bsm_ivols_from_slice_prices 16 | 17 | 18 | # @njit 19 | def generate_tvars_stock_path(nu: float = 4.5, 20 | n_path: int = 10000, 21 | ttm: float = 1.0 / 12.0, 22 | vol: float = 0.2 23 | ) -> np.ndarray: 24 | rv = np.random.standard_t(df=nu, size=n_path) 25 | upsilon = compute_upsilon(vol=vol, ttm=ttm, nu=nu) 26 | prices_t = 1.0 + upsilon*rv 27 | prices_t = prices_t + (1.0 - np.nanmean(prices_t)) 28 | return prices_t 29 | 30 | 31 | @njit 32 | def compute_kernel(prices_t: np.ndarray, b: float, 33 | nu: float = 4.5, 34 | ttm: float = 1.0 / 12.0, 35 | vol: float = 0.2 36 | ) -> np.ndarray: 37 | """ 38 | spot = 1 + x => x = spot - 1 39 | """ 40 | x = prices_t - 1 41 | coeff = 3.0*ttm*vol*vol*(nu-2.0) / (nu-4.0) 42 | a = -b*coeff 43 | kernel = 1.0 + a*x+b*x*x*x 44 | kernel = np.where(kernel > 0.0, kernel, 0.0001) 45 | return kernel 46 | 47 | 48 | def compute_implied_vols(b: float = -1, 49 | nu: float = 4.5, 50 | n_path: int = 100000, 51 | ttm: float = 1.0 / 12.0, 52 | vol: float = 0.2 53 | ) -> pd.DataFrame: 54 | 55 | prices_t = generate_tvars_stock_path(nu=nu, n_path=n_path, ttm=ttm, vol=vol) 56 | kernel = compute_kernel(prices_t=prices_t, b=b, nu=nu, ttm=ttm, vol=vol) 57 | kernel = kernel / np.nanmean(kernel*prices_t) 58 | print(np.nanmean(kernel*prices_t)) 59 | 60 | strikes = np.linspace(0.5, 1.5, 40) 61 | optiontypes = np.where(strikes < 1.0, 'P', 'C') 62 | 63 | model_prices = np.zeros_like(strikes) 64 | model_prices_kernel = np.zeros_like(strikes) 65 | for idx, (strike, type_) in enumerate(zip(strikes, optiontypes)): 66 | if type_ == 'C': 67 | payoff = np.where(np.greater(prices_t, strike), prices_t-strike, 0.0) 68 | else: 69 | payoff = np.where(np.less(prices_t, strike), strike - prices_t, 0.0) 70 | model_prices[idx] = np.nanmean(payoff) 71 | model_prices_kernel[idx] = np.nanmean(kernel*payoff) 72 | 73 | bsm_vols = infer_bsm_ivols_from_slice_prices(ttm=ttm, forward=1.0, strikes=strikes, 74 | optiontypes=optiontypes, 75 | model_prices=model_prices, 76 | discfactor=1.0) 77 | print(model_prices_kernel) 78 | bsm_vols_kernel = infer_bsm_ivols_from_slice_prices(ttm=ttm, forward=1.0, strikes=strikes, 79 | optiontypes=optiontypes, 80 | model_prices=model_prices_kernel, 81 | discfactor=1.0) 82 | 83 | bsm_vols = pd.Series(bsm_vols, index=strikes, name='T-vols') 84 | bsm_vols_kernel = pd.Series(bsm_vols_kernel, index=strikes, name='T-vols - kernel') 85 | vols = pd.concat([bsm_vols, bsm_vols_kernel], axis=1) 86 | 87 | return vols 88 | 89 | 90 | bsm_vols = compute_implied_vols(b=-10.0, 91 | nu=5.0, 92 | ttm=1.0 / 12.0, 93 | n_path=500000) 94 | 95 | qis.plot_line(df=bsm_vols) 96 | 97 | plt.show() 98 | -------------------------------------------------------------------------------- /my_papers/volatility_models/README.md: -------------------------------------------------------------------------------- 1 | This module contains analysis for paper 2 | [What is a robust stochastic volatility model](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4647027) by Artur Sepp and Parviz Rakhmonov 3 | 4 | All figures in the paper are produced by unittests in 5 | ```python 6 | article_figures.py 7 | ``` 8 | https://github.com/ArturSepp/StochVolModels/blob/main/my_papers/volatility_models/article_figures.py 9 | 10 | See the description of data and analysis in the paper. 11 | -------------------------------------------------------------------------------- /my_papers/volatility_models/article_figures.py: -------------------------------------------------------------------------------- 1 | import string 2 | import numpy as np 3 | import pandas as pd 4 | import matplotlib.pyplot as plt 5 | import qis 6 | import seaborn as sns 7 | from typing import Dict, List 8 | from enum import Enum 9 | 10 | # package 11 | from stochvolmodels import LogSvParams 12 | from stochvolmodels.utils.funcs import set_seed 13 | 14 | # project 15 | from my_papers.volatility_models.load_data import fetch_ohlc_vol 16 | import my_papers.volatility_models.ss_distribution_fit as ssd 17 | from my_papers.volatility_models.vol_beta import estimate_vol_beta 18 | from my_papers.volatility_models.autocorr_fit import autocorr_fit_report_logsv 19 | 20 | KWARGS = dict(fontsize=14) 21 | FIGSIZE = (18, 8) 22 | 23 | 24 | def plot_vols(tickers: List[str]) -> plt.Figure: 25 | 26 | vols = {} 27 | for ticker in tickers: 28 | vol, returns = fetch_ohlc_vol(ticker=ticker) 29 | vols[ticker] = vol 30 | vols = pd.DataFrame.from_dict(vols, orient='columns') 31 | 32 | with sns.axes_style("darkgrid"): 33 | fig, axs = plt.subplots(1, 2, figsize=FIGSIZE, tight_layout=True) 34 | 35 | qis.plot_time_series(df=vols, 36 | x_date_freq='A', 37 | title="(A) Time Series", 38 | var_format='{:,.0%}', 39 | legend_loc='upper center', 40 | legend_stats=qis.LegendStats.AVG_NONNAN_LAST, 41 | y_limits=(0.0, None), 42 | ax=axs[0], 43 | **KWARGS) 44 | 45 | qis.plot_histogram(vols, 46 | xlabel='Vol', 47 | title="(B) Empirical PDF", 48 | legend_loc='upper center', 49 | xvar_format='{:,.0%}', 50 | desc_table_type=qis.DescTableType.NONE, 51 | x_limits=(0.0, 2.5), 52 | ax=axs[1], 53 | **KWARGS) 54 | 55 | return fig 56 | 57 | 58 | def plot_autocorrs(model_params: Dict[str, LogSvParams], 59 | nb_path: int = 5000, 60 | num_lags: int = 120, 61 | ttm: float = 10.0 62 | ) -> plt.Figure: 63 | 64 | with sns.axes_style("darkgrid"): 65 | fig, axs = plt.subplots(1, len(model_params.keys()), figsize=FIGSIZE, tight_layout=True) 66 | 67 | for idx, (ticker, logsv_params) in enumerate(model_params.items()): 68 | vol, returns = fetch_ohlc_vol(ticker=ticker) 69 | autocorr_fit_report_logsv(vol=vol, params=logsv_params, nb_path=nb_path, num_lags=num_lags, ttm=ttm, 70 | ax=axs[idx], 71 | **KWARGS) 72 | qis.set_title(ax=axs[idx], title=f"{string.ascii_uppercase[idx]}) {ticker}", **KWARGS) 73 | qis.align_y_limits_axs(axs) 74 | 75 | return fig 76 | 77 | 78 | def plot_ss_distributions(model_params: Dict[str, LogSvParams], 79 | bins: int = 50 80 | ) -> plt.Figure: 81 | 82 | with sns.axes_style("darkgrid"): 83 | if len(model_params.keys()) == 4: 84 | fig, axs = plt.subplots(2, 2, figsize=(18, 14), tight_layout=True) 85 | axs = qis.to_flat_list(axs) 86 | else: 87 | fig, axs = plt.subplots(1, len(model_params.keys()), figsize=FIGSIZE, tight_layout=True) 88 | 89 | for idx, (ticker, logsv_params) in enumerate(model_params.items()): 90 | vol, returns = fetch_ohlc_vol(ticker=ticker) 91 | heston_params = ssd.fit_distribution_heston(vol=vol, bins=bins) 92 | ssd.plot_estimated_svs(vol=vol, logsv_params=logsv_params, heston_params=heston_params, bins=bins, 93 | ax=axs[idx], 94 | **KWARGS) 95 | qis.set_title(ax=axs[idx], title=f"{string.ascii_uppercase[idx]}) {ticker}", **KWARGS) 96 | qis.align_y_limits_axs(axs) 97 | 98 | return fig 99 | 100 | 101 | def vol_beta_plots(tickers: List[str], span: int = 65) -> plt.Figure: 102 | 103 | vol_betas = {} 104 | for idx, ticker in enumerate(tickers): 105 | vol, returns = fetch_ohlc_vol(ticker=ticker) 106 | vol_betas[ticker] = estimate_vol_beta(vol=vol, returns=returns, span=span) 107 | vol_betas = pd.DataFrame.from_dict(vol_betas, orient='columns') 108 | 109 | with sns.axes_style("darkgrid"): 110 | fig, axs = plt.subplots(1, 2, figsize=FIGSIZE, tight_layout=True) 111 | qis.plot_time_series(vol_betas, 112 | x_date_freq='A', 113 | legend_stats=qis.LegendStats.AVG_NONNAN_LAST, 114 | trend_line=qis.TrendLine.ABOVE_ZERO_SHADOWS, 115 | title="(A) Time series", 116 | date_format='%d-%b-%y', 117 | legend_loc='upper left', 118 | framealpha=0.75, 119 | ax=axs[0], 120 | **KWARGS) 121 | 122 | # fig, ax = plt.subplots(1, 1, figsize=FIGSIZE, tight_layout=True) 123 | qis.plot_histogram(vol_betas, 124 | xlabel='Vol beta', 125 | title="(B) Empirical PDF", 126 | legend_loc='upper center', 127 | desc_table_type=qis.DescTableType.NONE, 128 | ax=axs[1], 129 | **KWARGS) 130 | 131 | return fig 132 | 133 | 134 | class UnitTests(Enum): 135 | PLOT_VOLS = 1 136 | AUTOCORRELATION_PLOTS = 2 137 | AUTOCORRELATION_PLOTS_BTC = 3 138 | SS_DENSITY_PLOTS = 4 139 | VOL_BETA_PLOTS = 5 140 | MODEL_PARAMS_TABLE = 6 141 | 142 | 143 | def run_unit_test(unit_test: UnitTests): 144 | 145 | set_seed(3) 146 | np.random.seed(3) 147 | 148 | vix_log_params = LogSvParams(sigma0=0.19928505844247962, theta=0.19928505844247962, kappa1=1.2878835150774184, 149 | kappa2=1.9267876555824357, beta=0.0, volvol=0.7210463316739526) 150 | 151 | move_log_params = LogSvParams(sigma0=0.9109917133860931, theta=0.9109917133860931, kappa1=0.1, 152 | kappa2=0.41131244621275886, beta=0.0, volvol=0.3564212939473691) 153 | 154 | ovx_log_params = LogSvParams(sigma0=0.3852514800317871, theta=0.3852514800317871, kappa1=2.7774564907918027, 155 | kappa2=2.2351296851221107, beta=0.0, volvol=0.8344408577025486) 156 | 157 | btc_log_params = LogSvParams(sigma0=0.7118361434192538, theta=0.7118361434192538, 158 | kappa1=2.214702576955766, kappa2=2.18028273418397, beta=0.0, volvol=0.921487415907961) 159 | 160 | eth_log_params = LogSvParams(sigma0=0.8657438901704476, theta=0.8657438901704476, kappa1=1.955809653686808, 161 | kappa2=1.978367101612294, beta=0.0, volvol=0.8484117641903834) 162 | 163 | model_params = {'VIX': vix_log_params, 164 | 'MOVE': move_log_params, 165 | 'OVX': ovx_log_params, 166 | 'BTC': btc_log_params} 167 | 168 | model_params_btc = {'VIX': vix_log_params, 169 | 'MOVE': move_log_params, 170 | 'OVX': ovx_log_params, 171 | 'BTC': btc_log_params} 172 | 173 | local_path = "C://Users//artur//OneDrive//My Papers//Working Papers//Review of Beta Lognormal SV Model. Zurich. Nov 2023//figures//" 174 | 175 | if unit_test == UnitTests.PLOT_VOLS: 176 | fig = plot_vols(tickers=list(model_params.keys())) 177 | is_save = True 178 | if is_save: 179 | qis.save_fig(fig, file_name='vols_ts', local_path=local_path) 180 | 181 | elif unit_test == UnitTests.AUTOCORRELATION_PLOTS: 182 | fig = plot_autocorrs(model_params=model_params, 183 | nb_path=10000, num_lags=120, ttm=10.0) 184 | is_save = True 185 | if is_save: 186 | qis.save_fig(fig, file_name='autocorr_fit', local_path=local_path) 187 | 188 | elif unit_test == UnitTests.AUTOCORRELATION_PLOTS_BTC: 189 | fig = plot_autocorrs(model_params=model_params, 190 | nb_path=10000, num_lags=120, ttm=10.0) 191 | is_save = True 192 | if is_save: 193 | qis.save_fig(fig, file_name='autocorr_fit', local_path=local_path) 194 | 195 | elif unit_test == UnitTests.SS_DENSITY_PLOTS: 196 | fig = plot_ss_distributions(model_params=model_params) 197 | is_save = True 198 | if is_save: 199 | qis.save_fig(fig, file_name='ss_distribution', local_path=local_path) 200 | 201 | elif unit_test == UnitTests.VOL_BETA_PLOTS: 202 | tickers = ['VIX', 'MOVE', 'OVX', 'BTC'] 203 | fig = vol_beta_plots(tickers=tickers) 204 | is_save = True 205 | if is_save: 206 | qis.save_fig(fig, file_name='vol_beta', local_path=local_path) 207 | 208 | elif unit_test == UnitTests.MODEL_PARAMS_TABLE: 209 | data = {ticker: model_param.to_dict() for ticker, model_param in model_params.items() } 210 | df = pd.DataFrame.from_dict(data, orient='columns') 211 | df = df.drop(['sigma0', 'beta'], axis=0) 212 | dfs = qis.df_to_str(df=df) 213 | print(dfs) 214 | text = dfs.to_latex() 215 | print(text) 216 | 217 | plt.show() 218 | 219 | 220 | if __name__ == '__main__': 221 | 222 | unit_test = UnitTests.MODEL_PARAMS_TABLE 223 | 224 | is_run_all_tests = False 225 | if is_run_all_tests: 226 | for unit_test in UnitTests: 227 | run_unit_test(unit_test=unit_test) 228 | else: 229 | run_unit_test(unit_test=unit_test) 230 | -------------------------------------------------------------------------------- /my_papers/volatility_models/autocorr_fit.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import matplotlib.pyplot as plt 4 | import qis as qis 5 | from typing import Tuple 6 | from scipy.optimize import minimize 7 | from enum import Enum 8 | 9 | import my_papers.volatility_models.ss_distribution_fit as ssd 10 | from my_papers.volatility_models.load_data import fetch_ohlc_vol 11 | from stochvolmodels.pricers.logsv_pricer import LogSVPricer 12 | from stochvolmodels import LogSvParams 13 | from stochvolmodels.utils.funcs import set_seed 14 | 15 | 16 | def compute_autocorr_power(alpha: float = 0.1, c: float = 1.0, num_lags: int = 20) -> np.ndarray: 17 | dts = np.arange(0, num_lags) / 260.0 18 | pf = 1.0 - c*np.power(dts, 2.0*alpha+1) 19 | return pf 20 | 21 | 22 | def fit_autocorr_power(vol: pd.Series, num_lags: int = 60) -> Tuple[float, float]: 23 | 24 | # estimate empirical acf 25 | empirical = qis.compute_path_autocorr(a=vol.to_numpy(), num_lags=num_lags) 26 | 27 | def objective(pars: np.ndarray, args: np.ndarray) -> float: 28 | alpha, c = pars[0], pars[1] 29 | model_acfs = compute_autocorr_power(alpha=alpha, c=c, num_lags=num_lags) 30 | sse = np.nansum(np.square(model_acfs - empirical)) 31 | return sse 32 | 33 | options = {'disp': True, 'ftol': 1e-8} 34 | p0 = np.array([0.1, 0.99]) 35 | bounds = ((-0.5, 0.5), (0.01, 1.5)) 36 | res = minimize(objective, p0, args=None, method='SLSQP', bounds=bounds, options=options) 37 | fit_alpha, fit_c = res.x[0], res.x[1] 38 | return fit_alpha, fit_c 39 | 40 | 41 | def simulate_autocorr(params: LogSvParams, 42 | brownians: np.ndarray = None, 43 | nb_path: int = 1000, 44 | nb_steps: int = 260, 45 | num_lags: int = 20, 46 | ttm: float = 10.0 47 | ) -> np.ndarray: 48 | """ 49 | use mc paths to compute paths of autocorrelation 50 | """ 51 | logsv_pricer = LogSVPricer() 52 | if brownians is None: 53 | brownians = np.sqrt(1.0 / 260) * np.random.normal(0, 1, size=(nb_steps, nb_path)) 54 | sigma_t, grid_t = logsv_pricer.simulate_vol_paths(params=params, 55 | nb_path=nb_path, 56 | nb_steps=nb_steps, 57 | ttm=ttm, 58 | brownians=brownians) 59 | acfs = qis.compute_path_autocorr(a=sigma_t, num_lags=num_lags) 60 | return acfs 61 | 62 | 63 | def get_brownians(nb_steps: int, nb_path: int) -> np.ndarray: 64 | try: 65 | brownians = get_brownians.brownians 66 | except AttributeError: # read static data and save for next call 67 | dt = 1.0 / 260.0 68 | brownians = np.sqrt(dt) * np.random.normal(0, 1, size=(nb_steps, nb_path)) 69 | get_brownians.brownians = brownians 70 | return brownians 71 | 72 | 73 | def fit_autocorr_logsv(vol: pd.Series, 74 | nb_path: int = 1000, 75 | num_lags: int = 60, 76 | ttm: float = 10.0 77 | ) -> LogSvParams: 78 | """ 79 | fit autocorrelation of log sv model using MC simulations 80 | """ 81 | # fix brownians 82 | nb_steps = int(260*ttm) 83 | brownians = get_brownians(nb_steps=nb_steps, nb_path=nb_path) 84 | 85 | # estimate empirical acf 86 | empirical = qis.compute_path_autocorr(a=vol.to_numpy(), num_lags=num_lags) 87 | 88 | def unpack_pars(pars: np.ndarray) -> LogSvParams: 89 | kappa1, kappa2 = pars[0], pars[1] 90 | params = ssd.fit_distribution_log_sv_fixed_kappa(vol=vol, kappa1=kappa1, kappa2=kappa2) 91 | return params 92 | 93 | def objective(pars: np.ndarray, args: np.ndarray) -> float: 94 | params = unpack_pars(pars=pars) 95 | model_acfs = simulate_autocorr(params=params, 96 | brownians=brownians, 97 | nb_path=nb_path, 98 | nb_steps=nb_steps, 99 | num_lags=num_lags, 100 | ttm=ttm) 101 | model_acfs = np.mean(model_acfs, axis=1) 102 | sse = np.nansum(np.square(model_acfs - empirical)) 103 | return sse 104 | 105 | options = {'disp': True, 'ftol': 1e-8} 106 | p0 = np.array([2.0, 2.0]) 107 | bounds = ((0.2, 10), (0.2, 10)) 108 | res = minimize(objective, p0, args=None, method='SLSQP', bounds=bounds, options=options) 109 | 110 | fit_params = unpack_pars(pars=res.x) 111 | return fit_params 112 | 113 | 114 | def autocorr_fit_report_logsv(vol: pd.Series, 115 | params: LogSvParams, 116 | nb_path: int = 5000, 117 | num_lags: int = 90, 118 | ttm: float = 10.0, 119 | ax: plt.Subplot = None, 120 | **kwargs 121 | ) -> None: 122 | 123 | # estimate empirical acf 124 | index = range(0, num_lags) 125 | empirical = qis.compute_path_autocorr(a=vol.to_numpy(), num_lags=num_lags) 126 | empirical = pd.Series(empirical, index=index, name='Empirical') 127 | 128 | # simulated 129 | nb_steps = int(260*ttm) 130 | brownians = get_brownians(nb_steps=nb_steps, nb_path=nb_path) 131 | model_acfs = simulate_autocorr(params=params, 132 | brownians=brownians, 133 | nb_path=nb_path, 134 | nb_steps=nb_steps, 135 | num_lags=num_lags, 136 | ttm=ttm) 137 | model_acf = np.mean(model_acfs, axis=1) 138 | model_acf = pd.Series(model_acf, index=index, name='Log SV') 139 | 140 | # power 141 | alpha, c = fit_autocorr_power(vol=vol, num_lags=num_lags) 142 | pf = compute_autocorr_power(alpha=alpha, c=c, num_lags=num_lags) 143 | pf_power = pd.Series(pf, index=index, name='Rough ' + r'$\alpha$' + f"={alpha:0.2f}") 144 | 145 | # plot acfc 146 | acfs_df = pd.concat([empirical, model_acf, pf_power], axis=1) 147 | if ax is None: 148 | fig, ax = plt.subplots(1, 1, figsize=(18, 10), tight_layout=True) 149 | 150 | qis.plot_line(acfs_df, 151 | linestyles=['dotted', 'solid', 'dashed'], 152 | ax=ax, 153 | legend_loc='upper center', 154 | xlabel='Lag', 155 | **kwargs) 156 | 157 | 158 | class UnitTests(Enum): 159 | EMPIRICAL_AUTOCORR = 1 160 | AUTOCORR_POWER = 2 161 | FIT_AUTOCORR_LOGSV = 3 162 | FIT_AUTOCORR_HESTON = 4 163 | FIT_REPORT = 5 164 | 165 | 166 | def run_unit_test(unit_test: UnitTests): 167 | 168 | set_seed(3) 169 | np.random.seed(3) 170 | 171 | vix_log_params = LogSvParams(sigma0=0.19928505844247962, theta=0.19928505844247962, kappa1=1.2878835150774184, 172 | kappa2=1.9267876555824357, beta=0.0, volvol=0.7210463316739526) 173 | 174 | move_log_params = LogSvParams(sigma0=0.9109917133860931, theta=0.9109917133860931, 175 | kappa1=0.1, kappa2=0.41131244621275886, beta=0.0, volvol=0.3564212939473691) 176 | 177 | ovx_log_params = LogSvParams(sigma0=0.3852514800317871, theta=0.3852514800317871, kappa1=2.7774564907918027, 178 | kappa2=2.2351296851221107, beta=0.0, volvol=0.8344408577025486) 179 | 180 | btc_log_params = LogSvParams(sigma0=0.7118361434192538, theta=0.7118361434192538, 181 | kappa1=2.214702576955766, kappa2=2.18028273418397, beta=0.0, volvol=0.921487415907961) 182 | 183 | eth_log_params = LogSvParams(sigma0=0.8657438901704476, theta=0.8657438901704476, kappa1=1.955809653686808, 184 | kappa2=1.978367101612294, beta=0.0, volvol=0.8484117641903834) 185 | 186 | nb_path: int = 10000 187 | num_lags: int = 120 188 | ttm: float = 10.0 189 | 190 | if unit_test == UnitTests.EMPIRICAL_AUTOCORR: 191 | vol, returns = fetch_ohlc_vol(ticker='VIX') 192 | empirical = qis.compute_path_autocorr(a=vol.to_numpy(), num_lags=num_lags) 193 | print(empirical) 194 | qis.plot_line(df=pd.Series(empirical)) 195 | 196 | dvols = vol.diff(1) 197 | vol_returns = qis.compute_path_corr(a1=dvols.to_numpy(), a2=returns.to_numpy(), num_lags=num_lags)[1:] 198 | print(vol_returns) 199 | qis.plot_line(df=pd.Series(vol_returns)) 200 | 201 | elif unit_test == UnitTests.AUTOCORR_POWER: 202 | vol, returns = fetch_ohlc_vol(ticker='OVX') 203 | alpha, c = fit_autocorr_power(vol=vol) 204 | print(f"alpha={alpha}, c={c}") 205 | pf = compute_autocorr_power(alpha=alpha, num_lags=60) 206 | print(pf) 207 | 208 | elif unit_test == UnitTests.FIT_AUTOCORR_LOGSV: 209 | vol, returns = fetch_ohlc_vol(ticker='ETH') 210 | fit_params = fit_autocorr_logsv(vol=vol, nb_path=nb_path, num_lags=num_lags, ttm=ttm) 211 | print(f"fit_params={fit_params}") 212 | qis.plot_time_series(df=vol) 213 | autocorr_fit_report_logsv(params=fit_params, vol=vol, nb_path=nb_path, num_lags=num_lags, ttm=ttm) 214 | ssd.plot_estimated_svs(vol=vol, logsv_params=fit_params, heston_params=None, bins=50) 215 | 216 | elif unit_test == UnitTests.FIT_REPORT: 217 | vol, returns = fetch_ohlc_vol(ticker='VIX') 218 | autocorr_fit_report_logsv(params=vix_log_params, 219 | vol=vol, 220 | nb_path=nb_path, 221 | num_lags=120, 222 | ttm=ttm) 223 | 224 | plt.show() 225 | 226 | 227 | if __name__ == '__main__': 228 | 229 | unit_test = UnitTests.FIT_AUTOCORR_LOGSV 230 | 231 | is_run_all_tests = False 232 | if is_run_all_tests: 233 | for unit_test in UnitTests: 234 | run_unit_test(unit_test=unit_test) 235 | else: 236 | run_unit_test(unit_test=unit_test) -------------------------------------------------------------------------------- /my_papers/volatility_models/load_data.py: -------------------------------------------------------------------------------- 1 | """ 2 | fetch vol data either using historical ohlc vol or VIX and the likes 3 | """ 4 | import numpy as np 5 | import pandas as pd 6 | import qis 7 | import yfinance as yf 8 | from typing import Optional, Tuple 9 | from qis import OhlcEstimatorType 10 | 11 | 12 | def fetch_ohlc_vol(ticker: str = 'SPY', 13 | af: float = 260, 14 | timeperiod: Optional[qis.TimePeriod] = qis.TimePeriod('31Dec1999', None), 15 | ohlc_estimator_type: OhlcEstimatorType = OhlcEstimatorType.ROGERS_SATCHELL 16 | ) -> Tuple[pd.Series, pd.Series]: 17 | if ticker in ['VIX', 'MOVE', 'OVX']: # use implied indices 18 | ohlc_data = yf.download(tickers=f"^{ticker}", start=None, end=None, ignore_tz=True) 19 | ohlc_data.index = ohlc_data.index.tz_localize('UTC').tz_convert('UTC') 20 | vol = ohlc_data['Close'] / 100.0 21 | 22 | if ticker == 'VIX': 23 | spot_ticker = '^GSPC' # s&p 500 index 24 | elif ticker == 'MOVE': 25 | spot_ticker = '^TNX' # 10y rate 26 | elif ticker == 'OVX': 27 | spot_ticker = 'USO' # oil fund 28 | else: 29 | raise NotImplementedError 30 | 31 | prices = yf.download(tickers=spot_ticker, start=None, end=None, ignore_tz=True)['Adj Close'] 32 | prices.index = prices.index.tz_localize('UTC').tz_convert('UTC') 33 | 34 | if ticker == 'MOVE': 35 | returns = prices.diff(1) 36 | else: 37 | # returns = np.log(prices).diff(1) 38 | returns = prices.pct_change() 39 | 40 | elif ticker in ['BTC', 'ETH']: # use implied atm vols from internal data 41 | df = qis.load_df_from_csv(file_name=f"{ticker}_atm_vols", 42 | local_path="C://Users//Artur//OneDrive//analytics//resources//") 43 | prices = df.iloc[:, 0] 44 | vol = df.iloc[:, -1] 45 | returns = prices.pct_change() 46 | 47 | else: # use historical vol 48 | data = yf.download(tickers=ticker, start=None, end=None, ignore_tz=True) 49 | data.index = data.index.tz_localize('UTC').tz_convert('UTC') 50 | ohlc_data = data[['Open', 'High', 'Low', 'Close']].rename({'Open': 'open', 'High': 'high', 'Low': 'low', 'Close': 'close'}, axis=1) 51 | var = qis.estimate_ohlc_var(ohlc_data=ohlc_data, ohlc_estimator_type=ohlc_estimator_type) 52 | vol = np.sqrt(af*var) 53 | 54 | returns = np.log(data['Adj Close']).diff(1) 55 | 56 | vol = vol.replace([0.0, np.inf, -np.inf], np.nan).dropna() # drop outliers 57 | 58 | if timeperiod is not None: 59 | vol = timeperiod.locate(vol) 60 | returns = timeperiod.locate(returns) 61 | 62 | vol = vol.rename(ticker) 63 | returns = returns.rename(ticker) 64 | return vol, returns 65 | -------------------------------------------------------------------------------- /my_papers/volatility_models/vol_beta.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import matplotlib.pyplot as plt 3 | import seaborn as sns 4 | from enum import Enum 5 | import qis as qis 6 | 7 | from my_papers.volatility_models.load_data import fetch_ohlc_vol 8 | 9 | 10 | def estimate_vol_beta(vol: pd.Series, 11 | returns: pd.Series, 12 | span: int = 33 13 | ) -> pd.Series: 14 | # dvol = np.log(vol).diff(1).rename('dvol') 15 | dvol = vol.diff(1).rename('dvol') 16 | joint = pd.concat([dvol, returns], axis=1).dropna() 17 | vol_beta = qis.compute_one_factor_ewm_betas(x=joint[returns.name], y=joint['dvol'].to_frame(), 18 | span=span 19 | ).iloc[:, 0] 20 | return vol_beta 21 | 22 | 23 | def plot_vol_beta(vol: pd.Series, returns: pd.Series, span: int = 33): 24 | vol_beta = estimate_vol_beta(vol=vol, returns=returns, span=span) 25 | 26 | with sns.axes_style("darkgrid"): 27 | fig, ax = plt.subplots(1, 1, figsize=(18, 10), tight_layout=True) 28 | qis.plot_time_series(df=vol_beta, 29 | ax=ax) 30 | 31 | 32 | class UnitTests(Enum): 33 | VOL_BETA = 1 34 | PLOT_VOL_BETA = 2 35 | 36 | 37 | def run_unit_test(unit_test: UnitTests): 38 | 39 | if unit_test == UnitTests.VOL_BETA: 40 | vol, returns = fetch_ohlc_vol(ticker='VIX') 41 | vol_beta = estimate_vol_beta(vol=vol, returns=returns) 42 | print(vol_beta) 43 | 44 | elif unit_test == UnitTests.PLOT_VOL_BETA: 45 | vol, returns = fetch_ohlc_vol(ticker='OVX') 46 | plot_vol_beta(vol=vol, returns=returns) 47 | 48 | plt.show() 49 | 50 | 51 | if __name__ == '__main__': 52 | 53 | unit_test = UnitTests.PLOT_VOL_BETA 54 | 55 | is_run_all_tests = False 56 | if is_run_all_tests: 57 | for unit_test in UnitTests: 58 | run_unit_test(unit_test=unit_test) 59 | else: 60 | run_unit_test(unit_test=unit_test) 61 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "stochvolmodels" 3 | version = "1.0.29" 4 | description = "Implementation of stochastic volatility models for option pricing" 5 | license = "LICENSE.txt" 6 | authors = ["Artur Sepp "] 7 | maintainers = ["Artur Sepp "] 8 | readme = "README.md" 9 | repository = "https://github.com/ArturSepp/StochVolModels" 10 | documentation = "https://github.com/ArturSepp/StochVolModels" 11 | keywords= ["volatility", "options", "Black-Scholes", "Heston", "Monte-Carlo"] 12 | classifiers=[ 13 | "Development Status :: 4 - Beta", 14 | "Environment :: Console", 15 | "Intended Audience :: Financial and Insurance Industry", 16 | "Intended Audience :: Science/Research", 17 | "License :: OSI Approved :: MIT License", 18 | "Natural Language :: English", 19 | "Operating System :: OS Independent", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: 3.9", 22 | "Programming Language :: Python :: 3.8", 23 | "Programming Language :: Python :: 3 :: Only", 24 | "Topic :: Office/Business :: Financial :: Investment", 25 | ] 26 | 27 | packages = [ {include = "stochvolmodels"}] 28 | 29 | exclude = ["stochvolmodel/my_papers/"] 30 | 31 | [tool.poetry.urls] 32 | "Issues" = "https://github.com/ArturSepp/StochVolModels/issues" 33 | "Personal website" = "https://artursepp.com" 34 | 35 | [tool.poetry.dependencies] 36 | python = ">=3.8" 37 | numba = ">=0.55" 38 | numpy = ">=1.22.4" 39 | scipy = ">=1.3" 40 | pandas = ">=0.19" 41 | matplotlib = ">=3.5.2" 42 | seaborn = ">=0.11.2" 43 | 44 | [build-system] 45 | requires = ["poetry-core>=1.0.0"] 46 | build-backend = "poetry.core.masonry.api" 47 | -------------------------------------------------------------------------------- /read_modules.py: -------------------------------------------------------------------------------- 1 | import stochvolmodels.pricers.logsv.affine_expansion 2 | 3 | this = dir(stochvolmodels.pricers.logsv.affine_expansion) 4 | print(this) 5 | for x in this: 6 | if not any(y in x for y in ['__', 'Dict']): 7 | print(f"{x},") 8 | 9 | 10 | print('##############################') 11 | import inspect 12 | 13 | all_functions = inspect.getmembers(stochvolmodels.pricers.logsv.affine_expansion, inspect.isfunction) 14 | for x in all_functions: 15 | if not any(y in x for y in ['run_unit_test', 'njit', 'NamedTuple', 'dataclass', 'skew', 'kurtosis', 'abstractmethod']): 16 | print(f"{x[0]},") -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/StochVolModels/228ed94afd25a561f01afb5c1c7c5160454dc9f1/requirements.txt -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | # This includes the license file(s) in the wheel. 3 | # https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file 4 | license_files = LICENSE.txt -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | def read_requirements(file): 5 | with open(file) as f: 6 | return f.read().splitlines() 7 | 8 | 9 | def read_file(file): 10 | with open(file) as f: 11 | return f.read() 12 | 13 | 14 | long_description = read_file("README.md") 15 | requirements = read_requirements("requirements.txt") 16 | 17 | setup( 18 | name='stochvolmodels', 19 | version='1.0.29', 20 | author='Artur Sepp, Parviz Rakhmonov', 21 | author_email='artursepp@gmail.com, parviz.msu@gmail.com', 22 | url='https://github.com/ArturSepp/StochVolModels', 23 | description='Implementation of Stochastic Volatility Models', 24 | long_description_content_type="text/x-rst", # If this causes a warning, upgrade your setuptools package 25 | long_description=long_description, 26 | license="MIT license", 27 | packages=find_packages(exclude=["docs", "stochvolmodels/my_papers"]), # Don't include test directory in binary distribution 28 | install_requires=requirements, 29 | classifiers=[ 30 | "Programming Language :: Python :: 3", 31 | "License :: OSI Approved :: MIT License", 32 | "Operating System :: OS Independent", 33 | ] 34 | ) -------------------------------------------------------------------------------- /stochvolmodels/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from stochvolmodels.utils.config import VariableType 4 | 5 | from stochvolmodels.utils.mc_payoffs import compute_mc_vars_payoff 6 | 7 | from stochvolmodels.utils.mgf_pricer import (get_phi_grid, 8 | get_psi_grid, 9 | get_theta_grid, 10 | get_transform_var_grid, 11 | compute_integration_weights, 12 | vanilla_slice_pricer_with_mgf_grid, 13 | digital_slice_pricer_with_mgf_grid, 14 | slice_pricer_with_mgf_grid_with_gamma, 15 | slice_qvar_pricer_with_a_grid, 16 | pdf_with_mgf_grid) 17 | 18 | from stochvolmodels.utils.funcs import ( 19 | set_seed, 20 | compute_histogram_data, 21 | timer, 22 | to_flat_np_array, 23 | update_kwargs, 24 | ncdf, 25 | npdf, 26 | find_nearest 27 | ) 28 | 29 | from stochvolmodels.pricers.analytic.bsm import ( 30 | OptionType, 31 | compute_bsm_vanilla_price, 32 | compute_bsm_vanilla_slice_deltas, 33 | compute_bsm_vanilla_slice_prices, 34 | compute_bsm_forward_grid_prices, 35 | compute_bsm_vanilla_delta, 36 | compute_bsm_vanilla_grid_deltas, 37 | compute_bsm_strike_from_delta, 38 | compute_bsm_vanilla_deltas_ttms, 39 | compute_bsm_slice_vegas, 40 | compute_bsm_vegas_ttms, 41 | infer_bsm_implied_vol, 42 | infer_bsm_ivols_from_model_chain_prices, 43 | infer_bsm_ivols_from_model_slice_prices, 44 | infer_bsm_ivols_from_slice_prices 45 | ) 46 | 47 | from stochvolmodels.pricers.analytic.bachelier import ( 48 | compute_normal_delta, 49 | compute_normal_delta_from_lognormal_vol, 50 | compute_normal_delta_to_strike, 51 | compute_normal_deltas_ttms, 52 | compute_normal_price, 53 | compute_normal_slice_deltas, 54 | compute_normal_slice_prices, 55 | compute_normal_slice_vegas, 56 | compute_normal_vegas_ttms, 57 | infer_normal_implied_vol, 58 | infer_normal_ivols_from_chain_prices, 59 | infer_normal_ivols_from_model_slice_prices, 60 | infer_normal_ivols_from_slice_prices, 61 | ) 62 | 63 | from stochvolmodels.pricers.analytic.tdist import ( 64 | pdf_tdist, 65 | cdf_tdist, 66 | cum_mean_tdist, 67 | imply_drift_tdist, 68 | compute_default_prob_tdist, 69 | compute_forward_tdist, 70 | compute_vanilla_price_tdist, 71 | infer_implied_vol_tdist, 72 | infer_tdist_implied_vols_from_model_slice_prices 73 | ) 74 | 75 | from stochvolmodels.pricers.logsv.affine_expansion import ( 76 | ExpansionOrder, 77 | VariableType, 78 | compute_logsv_a_mgf_grid, 79 | func_a_ode_quadratic_terms, 80 | func_rhs, 81 | func_rhs_jac, 82 | get_expansion_n, 83 | get_init_conditions_a, 84 | solve_a_ode_grid, 85 | solve_analytic_ode_for_a, 86 | solve_analytic_ode_for_a0, 87 | solve_analytic_ode_grid_phi, 88 | solve_ode_for_a, 89 | compute_logsv_a_mgf_grid, 90 | solve_a_ode_grid, 91 | solve_ode_for_a, 92 | ) 93 | 94 | from stochvolmodels.pricers.hawkes_jd_pricer import ( 95 | HawkesJDParams, 96 | HawkesJDPricer 97 | ) 98 | 99 | from stochvolmodels.pricers.heston_pricer import ( 100 | BTC_HESTON_PARAMS, 101 | HestonParams, 102 | HestonPricer 103 | ) 104 | 105 | from stochvolmodels.pricers.logsv_pricer import ( 106 | LOGSV_BTC_PARAMS, 107 | LogSVPricer, 108 | LogsvModelCalibrationType, 109 | ConstraintsType, 110 | CalibrationEngine, 111 | get_randoms_for_chain_valuation, 112 | get_randoms_roughvol, 113 | logsv_mc_chain_pricer_fixed_randoms, 114 | logsv_roughmc_chain_pricer_fixed_randoms 115 | ) 116 | from stochvolmodels.pricers.logsv.logsv_params import LogSvParams 117 | 118 | from stochvolmodels.pricers.gmm_pricer import ( 119 | GmmParams, 120 | GmmPricer 121 | ) 122 | 123 | from stochvolmodels.pricers.tdist_pricer import ( 124 | TdistParams, 125 | TdistPricer 126 | ) 127 | 128 | 129 | from stochvolmodels.data.option_chain import OptionChain, OptionSlice 130 | 131 | 132 | from stochvolmodels.data.test_option_chain import ( 133 | get_btc_test_chain_data, 134 | get_gld_test_chain_data, 135 | get_gld_test_chain_data_6m, 136 | get_qv_options_test_chain_data, 137 | get_spy_test_chain_data, 138 | get_sqqq_test_chain_data, 139 | get_vix_test_chain_data 140 | ) 141 | 142 | from stochvolmodels.utils.plots import ( 143 | align_x_limits_axs, 144 | align_y_limits_axs, 145 | create_dummy_line, 146 | fig_list_to_pdf, 147 | fig_to_pdf, 148 | set_legend_colors, 149 | get_n_sns_colors, 150 | map_deltas_to_str, 151 | model_param_ts, 152 | model_vols_ts, 153 | plot_model_risk_var, 154 | save_fig, 155 | save_figs, 156 | set_fig_props, 157 | set_subplot_border, 158 | set_y_limits, 159 | vol_slice_fit 160 | ) 161 | 162 | 163 | from stochvolmodels.pricers.logsv.vol_moments_ode import compute_analytic_qvar 164 | -------------------------------------------------------------------------------- /stochvolmodels/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/StochVolModels/228ed94afd25a561f01afb5c1c7c5160454dc9f1/stochvolmodels/data/__init__.py -------------------------------------------------------------------------------- /stochvolmodels/data/fetch_option_chain.py: -------------------------------------------------------------------------------- 1 | """ 2 | this module is using option-chain-analytics package 3 | to fetch OptionChain data with options data 4 | see https://pypi.org/project/option-chain-analytics 5 | """ 6 | 7 | import pandas as pd 8 | import numpy as np 9 | import matplotlib.pyplot as plt 10 | from qis import TimePeriod 11 | from typing import Dict, Tuple, Optional, Literal 12 | from numba.typed import List 13 | from enum import Enum 14 | import qis as qis 15 | # chain 16 | from option_chain_analytics import OptionsDataDFs, create_chain_from_from_options_dfs 17 | from option_chain_analytics.option_chain import SliceColumn, SlicesChain 18 | # analytics 19 | from stochvolmodels.data.option_chain import OptionChain 20 | 21 | 22 | def generate_vol_chain_np(chain: SlicesChain, 23 | value_time: pd.Timestamp, 24 | days_map: Dict[str, int] = {'1w': 7, '1m': 21}, 25 | delta_bounds: Tuple[Optional[float], Optional[float]] = (-0.1, 0.1), 26 | is_filtered: bool = True 27 | ) -> OptionChain: 28 | """ 29 | given SlicesChain generate OptionChain for calibration inputs 30 | """ 31 | 32 | ttms, future_prices, discfactors = List(), List(), List() 33 | optiontypes_ttms, strikes_ttms = List(), List() 34 | bid_ivs, ask_ivs = List(), List() 35 | bid_prices, ask_prices = List(), List() 36 | slice_ids = [] 37 | for label, day in days_map.items(): 38 | next_date = value_time + pd.DateOffset(days=day) # if overlapping next date will be last avilable maturity 39 | slice_date = chain.get_next_slice_after_date(mat_date=next_date) 40 | slice_t = chain.expiry_slices[slice_date] 41 | df = slice_t.get_joint_slice(delta_bounds=delta_bounds, is_filtered=is_filtered) 42 | if not df.empty: 43 | slice_ids.append(f"{label}: {slice_t.expiry_id}") 44 | ttms.append(slice_t.get_ttm()) 45 | future_prices.append(slice_t.get_future_price()) 46 | discfactors.append(1.0) 47 | strikes_ttms.append(df.index.to_numpy()) 48 | optiontypes_ttms.append(df[SliceColumn.OPTION_TYPE].to_numpy(dtype=str)) 49 | bid_ivs.append(df[SliceColumn.BID_IV].to_numpy()) 50 | ask_ivs.append(df[SliceColumn.ASK_IV].to_numpy()) 51 | bid_prices.append(df[SliceColumn.BID_PRICE].to_numpy()) 52 | ask_prices.append(df[SliceColumn.ASK_PRICE].to_numpy()) 53 | 54 | out = OptionChain(ttms=np.array(ttms), 55 | forwards=np.array(future_prices), 56 | discfactors=np.array(discfactors), 57 | ids=np.array(slice_ids), 58 | strikes_ttms=strikes_ttms, 59 | optiontypes_ttms=optiontypes_ttms, 60 | bid_ivs=bid_ivs, 61 | ask_ivs=ask_ivs, 62 | bid_prices=bid_prices, 63 | ask_prices=ask_prices) 64 | return out 65 | 66 | 67 | def load_option_chain(options_data_dfs: OptionsDataDFs, 68 | value_time: pd.Timestamp = pd.Timestamp('2023-02-06 08:00:00+00:00'), 69 | days_map: Dict[str, int] = {'1w': 7, '1m': 21}, 70 | delta_bounds: Tuple[Optional[float], Optional[float]] = (-0.1, 0.1), 71 | is_filtered: bool = True 72 | ) -> Optional[OptionChain]: 73 | chain = create_chain_from_from_options_dfs(options_data_dfs=options_data_dfs, value_time=value_time) 74 | if chain is not None: 75 | option_chain = generate_vol_chain_np(chain=chain, 76 | value_time=value_time, 77 | days_map=days_map, 78 | delta_bounds=delta_bounds, 79 | is_filtered=is_filtered) 80 | else: 81 | option_chain = None 82 | 83 | return option_chain 84 | 85 | 86 | def sample_option_chain_at_times(options_data_dfs: OptionsDataDFs, 87 | time_period: TimePeriod, 88 | freq: str = 'W-FRI', 89 | days_map: Dict[str, int] = {'1w': 7, '1m': 21}, 90 | delta_bounds: Tuple[Optional[float], Optional[float]] = (-0.1, 0.1), 91 | hour_offset: int = 8 92 | ) -> Dict[pd.Timestamp, OptionChain]: 93 | value_times = qis.generate_dates_schedule(time_period=time_period, 94 | freq=freq, 95 | hour_offset=hour_offset) 96 | option_chains = {} 97 | for value_time in value_times: 98 | option_chains[value_time] = load_option_chain(options_data_dfs=options_data_dfs, 99 | value_time=value_time, 100 | days_map=days_map, 101 | delta_bounds=delta_bounds, 102 | is_filtered=True) 103 | return option_chains 104 | 105 | 106 | def load_price_data(options_data_dfs: OptionsDataDFs, 107 | time_period: TimePeriod = None, 108 | data: Literal['spot', 'perp', 'funding_rate'] = 'spot', 109 | freq: Optional[str] = 'D' # to do 110 | ) -> pd.Series: 111 | #options_data_dfs = OptionsDataDFs(**ts_data_loader_wrapper(ticker=ticker, freq='D', hour_offset=8)) 112 | spot_price = options_data_dfs.get_spot_data()[data] 113 | if freq is not None: 114 | spot_price = spot_price.resample(freq).last() 115 | if time_period is not None: 116 | spot_price = time_period.locate(spot_price) 117 | return spot_price 118 | 119 | 120 | class UnitTests(Enum): 121 | PRINT_CHAIN_DATA = 1 122 | GENERATE_VOL_CHAIN_NP = 2 123 | SAMPLE_CHAIN_AT_TIMES = 3 124 | 125 | 126 | def run_unit_test(unit_test: UnitTests): 127 | 128 | ticker = 'BTC' # BTC, ETH 129 | value_time = pd.Timestamp('2021-10-21 08:00:00+00:00') 130 | value_time = pd.Timestamp('2023-10-06 08:00:00+00:00') 131 | 132 | from option_chain_analytics.ts_loaders import ts_data_loader_wrapper 133 | options_data_dfs = OptionsDataDFs(**ts_data_loader_wrapper(ticker=ticker)) 134 | options_data_dfs.get_start_end_date().print() 135 | chain = create_chain_from_from_options_dfs(options_data_dfs=options_data_dfs, value_time=value_time) 136 | 137 | if unit_test == UnitTests.PRINT_CHAIN_DATA: 138 | for expiry, eslice in chain.expiry_slices.items(): 139 | eslice.print() 140 | 141 | elif unit_test == UnitTests.GENERATE_VOL_CHAIN_NP: 142 | option_chain = generate_vol_chain_np(chain=chain, 143 | value_time=value_time, 144 | days_map={'1w': 7}, 145 | delta_bounds=(-0.1, 0.1), 146 | is_filtered=True) 147 | option_chain.print() 148 | skews = option_chain.get_chain_skews(delta=0.35) 149 | print(skews) 150 | 151 | elif unit_test == UnitTests.SAMPLE_CHAIN_AT_TIMES: 152 | time_period = qis.TimePeriod('01Jan2023', '31Jan2023', tz='UTC') 153 | option_chains = sample_option_chain_at_times(options_data_dfs=options_data_dfs, 154 | time_period=time_period, 155 | freq='W-FRI', 156 | hour_offset=9 157 | ) 158 | for key, chain in option_chains.items(): 159 | print(f"{key}") 160 | print(chain) 161 | 162 | plt.show() 163 | 164 | 165 | if __name__ == '__main__': 166 | 167 | unit_test = UnitTests.SAMPLE_CHAIN_AT_TIMES 168 | 169 | is_run_all_tests = False 170 | if is_run_all_tests: 171 | for unit_test in UnitTests: 172 | run_unit_test(unit_test=unit_test) 173 | else: 174 | run_unit_test(unit_test=unit_test) 175 | -------------------------------------------------------------------------------- /stochvolmodels/examples/quick_run_lognormal_sv_pricer.py: -------------------------------------------------------------------------------- 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() 35 | -------------------------------------------------------------------------------- /stochvolmodels/examples/run_heston.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from stochvolmodels import HestonPricer, HestonParams, OptionChain 4 | 5 | # define parameters for bootstrap 6 | params_dict = {'rho=0.0': HestonParams(v0=0.2**2, theta=0.2**2, kappa=4.0, volvol=0.75, rho=0.0), 7 | 'rho=-0.4': HestonParams(v0=0.2**2, theta=0.2**2, kappa=4.0, volvol=0.75, rho=-0.4), 8 | 'rho=-0.8': HestonParams(v0=0.2**2, theta=0.2**2, kappa=4.0, volvol=0.75, rho=-0.8)} 9 | 10 | # get uniform slice 11 | option_chain = OptionChain.get_uniform_chain(ttms=np.array([0.25]), ids=np.array(['3m']), strikes=np.linspace(0.8, 1.15, 20)) 12 | option_slice = option_chain.get_slice(id='3m') 13 | 14 | # run pricer 15 | pricer = HestonPricer() 16 | pricer.plot_model_slices_in_params(option_slice=option_slice, params_dict=params_dict) 17 | 18 | plt.show() 19 | -------------------------------------------------------------------------------- /stochvolmodels/examples/run_heston_sv_pricer.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | import stochvolmodels as sv 5 | from stochvolmodels import HestonPricer, HestonParams, OptionChain, BTC_HESTON_PARAMS 6 | 7 | pricer = HestonPricer() 8 | 9 | # define model params 10 | params = HestonParams(v0=1.0, theta=1.0, kappa=5.0, volvol=1.0, rho=-0.5) 11 | 12 | # 1. one price 13 | model_price, vol = pricer.price_vanilla(params=params, 14 | ttm=0.25, 15 | forward=1.0, 16 | strike=1.0, 17 | optiontype='C') 18 | print(f"price={model_price:0.4f}, implied vol={vol: 0.2%}") 19 | 20 | # 2. price slice 21 | model_prices, vols = pricer.price_slice(params=params, 22 | ttm=0.25, 23 | forward=1.0, 24 | strikes=np.array([0.9, 1.0, 1.1]), 25 | optiontypes=np.array(['P', 'C', 'C'])) 26 | print([f"{p:0.4f}, implied vol={v: 0.2%}" for p, v in zip(model_prices, vols)]) 27 | 28 | # 3. prices for option chain with uniform strikes 29 | option_chain = OptionChain.get_uniform_chain(ttms=np.array([0.083, 0.25]), 30 | ids=np.array(['1m', '3m']), 31 | strikes=np.linspace(0.9, 1.1, 3)) 32 | model_prices, vols = pricer.compute_chain_prices_with_vols(option_chain=option_chain, params=params) 33 | print(model_prices) 34 | print(vols) 35 | 36 | # define uniform option chain 37 | option_chain = OptionChain.get_uniform_chain(ttms=np.array([0.083, 0.25]), 38 | ids=np.array(['1m', '3m']), 39 | strikes=np.linspace(0.5, 1.5, 21)) 40 | pricer.plot_model_ivols(option_chain=option_chain, 41 | params=params) 42 | 43 | 44 | # define uniform option chain 45 | option_chain = OptionChain.get_uniform_chain(ttms=np.array([0.083, 0.25]), 46 | ids=np.array(['1m', '3m']), 47 | strikes=np.linspace(0.5, 1.5, 21)) 48 | 49 | # define parameters for bootstrap 50 | params_dict = {'kappa=5': HestonParams(v0=1.0, theta=1.0, kappa=5.0, volvol=1.0, rho=-0.5), 51 | 'kappa=10': HestonParams(v0=1.0, theta=1.0, kappa=10.0, volvol=1.0, rho=-0.5)} 52 | option_slice = option_chain.get_slice(id='1m') 53 | pricer.plot_model_slices_in_params(option_slice=option_slice, 54 | params_dict=params_dict) 55 | 56 | 57 | btc_option_chain = sv.get_btc_test_chain_data() 58 | btc_calibrated_params = BTC_HESTON_PARAMS 59 | pricer.plot_model_ivols_vs_bid_ask(option_chain=btc_option_chain, 60 | params=btc_calibrated_params) 61 | 62 | btc_option_chain = sv.get_btc_test_chain_data() 63 | params0 = HestonParams(v0=0.8, theta=1.0, kappa=5.0, volvol=1.0, rho=-0.5) 64 | btc_calibrated_params = pricer.calibrate_model_params_to_chain(option_chain=btc_option_chain, 65 | params0=params0, 66 | constraints_type=sv.ConstraintsType.INVERSE_MARTINGALE) 67 | print(btc_calibrated_params) 68 | pricer.plot_model_ivols_vs_bid_ask(option_chain=btc_option_chain, 69 | params=btc_calibrated_params) 70 | plt.show() 71 | -------------------------------------------------------------------------------- /stochvolmodels/examples/run_pricing_options_on_qvar.py: -------------------------------------------------------------------------------- 1 | """ 2 | run valuation for options on quadratic variance 3 | """ 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | import stochvolmodels.data.test_option_chain as chains 7 | from numba.typed import List 8 | from stochvolmodels import (LogSVPricer, LogSvParams, compute_analytic_qvar, OptionChain, 9 | VariableType, HestonPricer, HestonParams) 10 | 11 | # these params are calibrated to the same BTC option chain 12 | # v0=theta=1 to have flat vol term structure 13 | LOGSV_BTC_PARAMS = LogSvParams(sigma0=1.0, theta=1.0, kappa1=3.1844, kappa2=3.058, beta=0.1514, volvol=1.8458) 14 | BTC_HESTON_PARAMS = HestonParams(v0=1.0, theta=1.0, kappa=7.4565, rho=0.0919, volvol=4.0907) 15 | 16 | ttms = {'1w': 1.0 / 52.0, '1m': 1.0 / 12.0, '3m': 0.25, '6m': 0.5} 17 | 18 | # get test strikes for qv 19 | option_chain = chains.get_qv_options_test_chain_data() 20 | option_chain = OptionChain.get_slices_as_chain(option_chain, ids=list(ttms.keys())) 21 | 22 | # compute forwards using QV model 23 | forwards = np.array([compute_analytic_qvar(params=LOGSV_BTC_PARAMS, ttm=ttm, n_terms=4) for ttm in ttms.values()]) 24 | print(f"QV forwards = {forwards}") 25 | # replace forwards to imply BSM vols 26 | option_chain.forwards = forwards 27 | # adjust strikes 28 | option_chain.strikes_ttms = List(forward * strikes_ttm for forward, strikes_ttm in 29 | zip(option_chain.forwards, option_chain.strikes_ttms)) 30 | 31 | nb_path = 200000 32 | 33 | # run log sv pricer 34 | logsv_pricer = LogSVPricer() 35 | fig1 = logsv_pricer.plot_model_ivols_vs_mc(option_chain=option_chain, 36 | params=LOGSV_BTC_PARAMS, 37 | variable_type=VariableType.Q_VAR, 38 | nb_path=nb_path) 39 | fig1.suptitle('Implied variance skew by Log-Normal SV model') 40 | 41 | # run Heston prices 42 | heston_pricer = HestonPricer() 43 | fig2 = heston_pricer.plot_model_ivols_vs_mc(option_chain=option_chain, 44 | params=BTC_HESTON_PARAMS, 45 | variable_type=VariableType.Q_VAR, 46 | nb_path=nb_path) 47 | fig2.suptitle('Implied variance skew by Heston SV model') 48 | 49 | 50 | plt.show() 51 | -------------------------------------------------------------------------------- /stochvolmodels/pricers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/StochVolModels/228ed94afd25a561f01afb5c1c7c5160454dc9f1/stochvolmodels/pricers/__init__.py -------------------------------------------------------------------------------- /stochvolmodels/pricers/analytic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/StochVolModels/228ed94afd25a561f01afb5c1c7c5160454dc9f1/stochvolmodels/pricers/analytic/__init__.py -------------------------------------------------------------------------------- /stochvolmodels/pricers/factor_hjm/double_exp_pricer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from typing import Tuple, Union 3 | 4 | 5 | def de_pricer(ff, ff_transf) -> Tuple[np.ndarray, np.ndarray]: 6 | eps0 = 1e-6 7 | h = 0.5 8 | eps = 1e-6 9 | Nmax = 12.0 10 | maxlev = 7 11 | 12 | s = func(ff, 0.0) 13 | # add terms for k > 0 14 | n1, s = trunc_index(ff, h2=h, delta=1, s=s, Nmax=Nmax, eps0=eps0) 15 | # add terms for k < 0 16 | n2, s = trunc_index(ff, h2=-h, delta=1, s=s, Nmax=Nmax, eps0=eps0) 17 | # level 0 estimate 18 | model_prices_ttm_prev = h * s 19 | model_ivs_ttm_prev = ff_transf(model_prices_ttm_prev)[1] 20 | m = 0 21 | err_ivol = 1 22 | model_prices_ttm = None 23 | model_ivs_ttm = None 24 | for m in np.arange(1.0, maxlev): 25 | h = h / 2.0 26 | # add terms in refined mesh for k > 0 27 | s1 = part_sum(ff, h2=h, delta=2, N=n1) 28 | # add terms in refined mesh for k < 0 29 | s2 = part_sum(ff, h2=-h, delta=2, N=n2) 30 | model_prices_ttm = 0.5 * model_prices_ttm_prev + h * (s1 + s2) 31 | # print(f"model_prices: {model_prices_ttm[0]}") 32 | model_ivs_ttm = ff_transf(model_prices_ttm)[1] 33 | err_ivol = np.linalg.norm(model_ivs_ttm - model_ivs_ttm_prev) 34 | rel_diff = np.linalg.norm(model_prices_ttm - model_prices_ttm_prev) <= eps * np.linalg.norm(model_prices_ttm) 35 | 36 | if rel_diff or err_ivol <= 1e-6: 37 | break 38 | else: 39 | # update 40 | model_prices_ttm_prev = model_prices_ttm 41 | model_ivs_ttm_prev = model_ivs_ttm 42 | n1 = 2 * n1 43 | n2 = 2 * n2 44 | # we calculate prices of capped payoff => need to transform into call option prices 45 | model_prices_ttm = ff_transf(model_prices_ttm)[0] 46 | if m == maxlev - 1 and err_ivol > 1e-6: 47 | # raise ValueError(f"errvol = {err_ivol:.6f}, error is above tolerance") 48 | # print(f"errvol = {err_ivol:.6f}, error is above tolerance") 49 | pass 50 | return model_prices_ttm, model_ivs_ttm 51 | 52 | 53 | def func(ff, x: Union[float, np.ndarray]) -> np.ndarray: 54 | """calculate the term w_k * f(x_k) of the DE scheme 55 | x must equal kh for some k""" 56 | if isinstance(x, float): 57 | x = np.array([x]) 58 | half_pi = 0.5 * np.pi 59 | exp_x = np.exp(x) 60 | sinh_x = 0.5 * (exp_x - 1.0 / exp_x) 61 | cosh_x = 0.5 * (exp_x + 1.0 / exp_x) 62 | exp_sinh_x = np.exp(half_pi * sinh_x) 63 | w_k = half_pi * cosh_x * exp_sinh_x 64 | x_k = exp_sinh_x 65 | val = (ff(x_k).T * w_k).T 66 | return val 67 | 68 | 69 | def part_sum(ff, h2: float, delta: int, N: int) -> float: 70 | s = 0.0 71 | func_vals = func(ff, h2 + np.arange(0.0, N, 1.0) * delta * h2) 72 | for idx, func_val in enumerate(func_vals): 73 | s = s + func_vals[idx] 74 | return s 75 | 76 | 77 | def trunc_index(ff, 78 | h2: float, 79 | delta: int, 80 | s: np.ndarray, 81 | Nmax: float, 82 | eps0: float) -> (int, np.ndarray): 83 | x = h2 84 | k = 1 85 | for k in np.arange(1.0, Nmax): 86 | xi = func(ff, x) 87 | s = s + xi 88 | if np.all(np.linalg.norm(xi, axis=0) <= eps0 * np.linalg.norm(s, axis=0)): 89 | break 90 | x = x + delta * h2 91 | return k, s 92 | 93 | 94 | -------------------------------------------------------------------------------- /stochvolmodels/pricers/factor_hjm/factor_hjm_pricer.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | import pandas as pd 4 | import seaborn as sns 5 | from enum import Enum 6 | from typing import Dict, Tuple 7 | from numba.typed import List 8 | 9 | import stochvolmodels.pricers.analytic.bachelier as bachel 10 | from stochvolmodels.pricers.factor_hjm.rate_logsv_params import MultiFactRateLogSvParams 11 | from stochvolmodels.pricers.factor_hjm.rate_core import get_default_swap_term_structure 12 | from stochvolmodels.pricers.factor_hjm.rate_logsv_pricer import simulate_logsv_MF, Measure 13 | 14 | 15 | def do_mc_simulation(basis_type: str, 16 | ccy: str, 17 | ttms: np.ndarray, 18 | x0: np.ndarray, 19 | y0: np.ndarray, 20 | I0: np.ndarray, 21 | sigma0: np.ndarray, 22 | params: MultiFactRateLogSvParams, 23 | nb_path: int, 24 | seed: int = None, 25 | measure_type: Measure = Measure.RISK_NEUTRAL, 26 | ts_sw: np.ndarray = None, 27 | bxs: np.ndarray = None, 28 | year_days: int = 360, 29 | T_fwd: float = None, 30 | ) -> Tuple[List[np.ndarray], List[np.ndarray], List[np.ndarray], List[np.ndarray]]: 31 | 32 | if basis_type != "NELSON-SIEGEL" : 33 | raise NotImplementedError 34 | x0s, y0s, I0s, sigma0s = simulate_logsv_MF(ttms=ttms, 35 | x0=x0, 36 | y0=y0, 37 | I0=I0, 38 | sigma0=sigma0, 39 | theta=params.theta, 40 | kappa1=params.kappa1, 41 | kappa2=params.kappa2, 42 | ts=params.ts, 43 | A=params.A, 44 | R=params.R, 45 | C=params.C, 46 | Omega=params.Omega, 47 | betaxs=params.beta.xs, 48 | volvolxs=params.volvol.xs, 49 | basis=params.basis, 50 | measure_type=measure_type, 51 | nb_path=nb_path, 52 | seed=seed, 53 | ccy=ccy, 54 | ts_sw=ts_sw, 55 | T_fwd=T_fwd, 56 | params0=params, 57 | bxs=bxs, 58 | year_days = year_days) 59 | 60 | return x0s, y0s, I0s, sigma0s 61 | 62 | 63 | def calc_mc_vols(basis_type: str, 64 | params: MultiFactRateLogSvParams, 65 | ttm: float, 66 | tenors: np.ndarray, 67 | forwards: List[np.ndarray], 68 | strikes_ttms: List[List[np.ndarray]], 69 | optiontypes: np.ndarray, 70 | is_annuity_measure: bool, 71 | nb_path: int, 72 | x0: np.ndarray = None, 73 | y0: np.ndarray = None, 74 | sigma0: np.ndarray = None, 75 | I0: np.ndarray = None, 76 | seed: int = None, 77 | x_in_delta_space: bool = False) -> (List[np.ndarray], List[np.ndarray]): 78 | # checks 79 | assert len(strikes_ttms) == len(tenors) 80 | assert len(strikes_ttms[0]) == 1 81 | assert len(forwards) == len(tenors) 82 | for fwd in forwards: 83 | assert fwd.shape == (1,) 84 | 85 | ttms = np.array([ttm]) 86 | # we simulate under risk-neutral measure only 87 | assert is_annuity_measure is False 88 | if x0 is None: 89 | x0 = np.zeros((nb_path, params.basis.get_nb_factors())) 90 | else: 91 | assert x0.shape == (nb_path, params.basis.get_nb_factors(),) 92 | if y0 is None: 93 | y0 = np.zeros((nb_path, params.basis.get_nb_aux_factors())) 94 | else: 95 | assert y0.shape == (nb_path, params.basis.get_nb_aux_factors(),) 96 | if sigma0 is None: 97 | sigma0 = np.ones((nb_path, 1)) 98 | else: 99 | assert sigma0.shape == (nb_path, 1) 100 | if I0 is None: 101 | I0 = np.zeros((nb_path,)) 102 | else: 103 | assert I0.shape == (nb_path,) 104 | 105 | ts_sws = [] 106 | bond0s = [] 107 | ann0s = [] 108 | swap0s = [] 109 | for tenor in tenors: 110 | ts_sw = get_default_swap_term_structure(expiry=ttm, tenor=tenor) 111 | ann0 = params.basis.annuity(t=ttm, ts_sw=ts_sw, x=x0, y=y0, ccy=params.ccy, m=0)[0] 112 | bond0 = params.basis.bond(0, ttm, x=x0, y=y0, ccy=params.ccy, m=0)[0] 113 | swap0 = params.basis.swap_rate(t=ttm, ts_sw=ts_sw, x=x0, y=y0, ccy=params.ccy)[0][0] 114 | ts_sws.append(ts_sw) 115 | bond0s.append(bond0) 116 | ann0s.append(ann0) 117 | swap0s.append(swap0) 118 | 119 | x0s, y0s, I0s, _ = do_mc_simulation(basis_type=basis_type, 120 | ccy=params.ccy, 121 | ttms=ttms, 122 | x0=x0, 123 | y0=y0, 124 | I0=I0, 125 | sigma0=sigma0, 126 | params=params, 127 | nb_path=nb_path, 128 | seed=None, 129 | measure_type=Measure.RISK_NEUTRAL) 130 | x0 = x0s[-1] 131 | y0 = y0s[-1] 132 | I0 = I0s[-1] 133 | mc_vols = List() 134 | mc_prices = List() 135 | mc_vols_ups = List() 136 | mc_vols_downs = List() 137 | std_factor = 1.96 138 | 139 | for idx_tenor, tenor in enumerate(tenors): 140 | ts_sw = ts_sws[idx_tenor] 141 | ann0 = ann0s[idx_tenor] 142 | bond0 = bond0s[idx_tenor] 143 | swap0 = swap0s[idx_tenor] 144 | strikes_ttm = strikes_ttms[idx_tenor][0] 145 | swap_mc, ann_mc, numer_mc = params.basis.calculate_swap_rate(ttm=ttm, x0=x0, y0=y0, I0=I0, ts_sw=ts_sw, 146 | ccy=params.ccy) 147 | # calculate option payoffs 148 | payoffsign = np.where(optiontypes == 'P', -1, 1).astype(float) 149 | option_mean = np.zeros_like(strikes_ttm) 150 | option_std = np.zeros_like(strikes_ttm) 151 | 152 | for idx, (strike, sign) in enumerate(zip(strikes_ttm, payoffsign)): 153 | option_mean[idx] = np.nanmean(1. / numer_mc * ann_mc * np.maximum(sign * (swap_mc - strike), 0)) / ann0 / bond0 154 | option_std[idx] = np.nanstd(1. / numer_mc * ann_mc * np.maximum(sign * (swap_mc - strike), 0)) / ann0 / bond0 155 | option_std[idx] = option_std[idx] / np.sqrt(nb_path) 156 | 157 | option_up = option_mean + std_factor * option_std 158 | option_down = np.maximum(option_mean - std_factor * option_std, 0.0) 159 | 160 | mc_ivols_mid = bachel.infer_normal_ivols_from_chain_prices(ttms=ttms, 161 | forwards=forwards, 162 | discfactors=np.ones_like(ttms), 163 | strikes_ttms=[strikes_ttm], 164 | optiontypes_ttms=[optiontypes], 165 | model_prices_ttms=[option_mean]) 166 | mc_ivols_up = bachel.infer_normal_ivols_from_chain_prices(ttms=ttms, 167 | forwards=forwards, 168 | discfactors=np.ones_like(ttms), 169 | strikes_ttms=[strikes_ttm], 170 | optiontypes_ttms=[optiontypes], 171 | model_prices_ttms=[option_up]) 172 | mc_ivols_down = bachel.infer_normal_ivols_from_chain_prices(ttms=ttms, 173 | forwards=forwards, 174 | discfactors=np.ones_like(ttms), 175 | strikes_ttms=[strikes_ttm], 176 | optiontypes_ttms=[optiontypes], 177 | model_prices_ttms=[option_down]) 178 | 179 | mc_vols.append(mc_ivols_mid[0]) 180 | mc_vols_ups.append(mc_ivols_up[0]) 181 | mc_vols_downs.append(mc_ivols_down[0]) 182 | 183 | mc_prices.append(option_mean) 184 | 185 | return mc_prices, mc_vols, mc_vols_ups, mc_vols_downs 186 | 187 | -------------------------------------------------------------------------------- /stochvolmodels/pricers/factor_hjm/rate_core.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numba import njit 3 | from typing import Union, Tuple 4 | 5 | 6 | # @njit(cache=False, fastmath=True) 7 | def bracket(ts: np.ndarray, t: float, throw_if_not_found: bool = False) -> int: 8 | # if 0 in ts: 9 | # raise ValueError("0 should be exluded") 10 | idx0 = -1 11 | for idx, tk in enumerate(ts): 12 | if t <= tk: 13 | idx0 = idx 14 | break 15 | if idx0 == -1 and throw_if_not_found: 16 | raise ValueError('t is not bracketed') 17 | return idx0 18 | 19 | 20 | #@njit(cache=False, fastmath=True) 21 | def pw_const(ts: np.ndarray, 22 | vs: np.ndarray, 23 | t: float, 24 | flat_extrapol: bool = False, 25 | shift: int = 0) -> Union[float, np.ndarray]: 26 | # if ts[0] < -1e-6 or ts[0] > 1e-6: 27 | # raise ValueError('first abscissa must be zero') 28 | assert shift == 0 or shift == 1 29 | if ts.shape[0] - shift != vs.shape[0]: 30 | raise ValueError('abcsissas and ordinates must have same shape') 31 | value = np.nan 32 | idx0 = bracket(ts[shift:], t, False) 33 | value = vs[idx0] 34 | if flat_extrapol and t >= ts[-1]: 35 | value = vs[-1] 36 | return value 37 | 38 | 39 | # @njit(cache=False, fastmath=True) 40 | def get_default_swap_term_structure(expiry: float, tenor: float): 41 | freq = 1.0 42 | return np.arange(expiry, expiry + tenor + freq, freq) # shift end by freq to include endpoint 43 | 44 | 45 | @njit(cache=False, fastmath=True) 46 | def get_futures_start_and_pmt(t0: float, lag: float, 47 | libor_tenor: float = 0.25) -> Tuple[float, float]: 48 | start = t0 + lag 49 | end = start + libor_tenor 50 | return start, end 51 | 52 | 53 | # numba version of curve related functions 54 | # @njit(cache=False, fastmath=True) 55 | def df_fast(t: Union[float, np.ndarray], ccy: str = "USD") -> float: 56 | # flat 0.8% zero rate for JPY and 3.5% for USD 57 | if ccy == "USD": 58 | r = 0.043 59 | elif ccy == "JPY": 60 | r = 0.008 61 | elif ccy == "USD_NS": 62 | lamda = 0.55/12 63 | beta1 = 0.0436 64 | beta2 = 0.013 65 | beta3 = -0.01 66 | 67 | t = np.maximum(t, 1e-4) 68 | r = beta1 + beta2*(1.0 - np.exp(-lamda * t)) / (lamda * t) + beta3*((1.0 - np.exp(-lamda * t)) / (lamda * t) - np.exp(-lamda * t)) 69 | 70 | 71 | else: 72 | raise NotImplementedError 73 | 74 | disc_factor = np.exp(-r * t) 75 | return disc_factor 76 | 77 | 78 | def bond_grad(bond_value, B_PX): 79 | bond_grad = np.zeros((bond_value.size, B_PX.size)) 80 | for j, bj in enumerate(B_PX): 81 | bond_grad[:, j] = bond_value * bj 82 | return bond_grad 83 | 84 | 85 | def swap_grad(numer0: np.ndarray, 86 | numer1: np.ndarray, 87 | denumer0: np.ndarray, 88 | denumer1: np.ndarray) -> np.ndarray: 89 | # scalar case is exceptional as we don't need to check input consistency 90 | if numer0.ndim == numer1.ndim == denumer0.ndim == denumer1.ndim: 91 | swap_grad = numer1 / denumer0 - (numer0 * denumer1) / np.power(denumer0, 2) 92 | return swap_grad 93 | 94 | assert numer0.ndim == 1 and denumer0.ndim == 1 95 | assert numer1.ndim == 2 and denumer1.ndim == 2 and np.all(numer1.shape == denumer1.shape) 96 | swap_grad = np.zeros_like(numer1) 97 | for j in range(numer1.shape[1]): 98 | swap_grad[:, j] = numer1[:, j] / denumer0 - (numer0 * denumer1[:, j]) / np.power(denumer0, 2) 99 | return swap_grad 100 | 101 | 102 | ############################################################# 103 | def generate_ttms_grid(ttms: np.ndarray, 104 | nb_pts: int = 11) -> np.ndarray: 105 | t0 = 0 106 | t_grid = np.array([0]) 107 | for idx, ttm in enumerate(ttms): 108 | t_grid0 = np.linspace(t0, ttm, nb_pts) 109 | t_grid = np.concatenate((t_grid, t_grid0[1:]), axis=None) 110 | t0 = ttm 111 | return t_grid 112 | 113 | 114 | @njit(cache=False, fastmath=True) 115 | def to_yearfrac(d1, d2): 116 | return d2 - d1 117 | ####################################################### 118 | 119 | 120 | def divide_mc(arr2d, arr1d): 121 | assert arr2d.ndim == 2 and arr1d.ndim == 1 122 | res = np.zeros_like(arr2d) 123 | for j, ann_derj in enumerate(arr2d.T): 124 | res[:, j] = ann_derj / arr1d 125 | return res 126 | 127 | 128 | def prod_mc(arr2d, arr1d): 129 | assert arr2d.ndim == 2 and arr1d.ndim == 1 130 | res = np.zeros_like(arr2d) 131 | for j, ann_derj in enumerate(arr2d.T): 132 | res[:, j] = ann_derj * arr1d 133 | return res 134 | 135 | 136 | # @njit(cache=False, fastmath=True) 137 | def bond(t: float, T: float, 138 | x: np.ndarray, y: np.ndarray, 139 | B_PX: np.ndarray, B_PY: np.ndarray, 140 | ccy: str, 141 | m: int = 0) -> np.ndarray: 142 | """return bond value (scalar) or gradient {dB/dx_i, i = 0,..,d} (vector)""" 143 | assert t <= T 144 | if x.ndim == 2 or y.ndim == 2: 145 | # dim of X is # simulations x #factors 146 | # assume that in matrix X the row is for trials 147 | # column is for factors 148 | assert x.shape[0] == y.shape[0] 149 | assert m == 0 or m == 1 150 | # because number function cannot return different types, we wrap scalar value into numpy array 151 | bond_value = np.array([df_fast(T, ccy) / df_fast(t, ccy) * np.exp(-B_PX.dot(np.transpose(x)) - B_PY.dot(np.transpose(y)))]) 152 | # make it just vector, not (1,nsims) matrix 153 | if x.ndim == 2 or y.ndim == 2: 154 | bond_value = bond_value[0, :] 155 | if m == 0: 156 | return bond_value 157 | elif m == 1: 158 | return bond_grad(bond_value, -B_PX) 159 | else: 160 | raise NotImplementedError 161 | 162 | 163 | def swap_rate(ccy: str, 164 | t: float, 165 | ts_sw: np.ndarray) -> np.ndarray: 166 | denumer0 = 0 167 | for i in range(1, ts_sw.size): 168 | denumer0 = denumer0 + (ts_sw[i] - ts_sw[i - 1]) * df_fast(ts_sw[i], ccy) / df_fast(t, ccy) 169 | 170 | numer0 = df_fast(ts_sw[0], ccy) / df_fast(t, ccy) - df_fast(ts_sw[-1], ccy) / df_fast(t, ccy) 171 | value0 = numer0 / denumer0 172 | 173 | return value0 174 | 175 | 176 | def libor_rate(ccy: str, 177 | t: float, tenor: float): 178 | zcb_start = df_fast(t, ccy=ccy) 179 | zcb_end = df_fast(t+tenor, ccy=ccy) 180 | libor = 1.0 / tenor * (zcb_start / zcb_end - 1.0) 181 | 182 | return libor 183 | 184 | 185 | @njit(cache=False, fastmath=True) 186 | def G(k, t, T): 187 | G_tT = (1.0 - np.exp(-k * (T - t))) / k 188 | return G_tT -------------------------------------------------------------------------------- /stochvolmodels/pricers/factor_hjm/rate_evaluate.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numba import njit 3 | from stochvolmodels.pricers.factor_hjm.rate_core import to_yearfrac 4 | 5 | 6 | @njit(cache=False, fastmath=True) 7 | def init_mean_rev(): 8 | return 0.025 9 | 10 | 11 | class Discount: 12 | def __init__(self, currency="USD"): 13 | self.today = 0 14 | # flat 0.8% zero rate for JPY and 3.5% for USD 15 | if currency == "USD": 16 | self.r = 0.043 17 | elif currency == "JPY": 18 | self.r = 0.008 19 | else: 20 | raise NotImplementedError 21 | 22 | def df(self, d) -> float: 23 | year_frac = to_yearfrac(self.today, d) 24 | disc_factor = np.exp(-self.r * year_frac) 25 | return disc_factor 26 | 27 | 28 | def G(t, T): 29 | k = init_mean_rev() 30 | G_tT = (1.0 - np.exp(-k * (T - t))) / k 31 | return G_tT 32 | 33 | 34 | def bond(t, T, x, y, 35 | m: int, 36 | is_mc_mode: bool, 37 | discount: Discount = None): 38 | if discount is None: 39 | discount = Discount() 40 | if isinstance(t, np.ndarray) and isinstance(x, np.ndarray) and t.shape != x.shape: 41 | raise ValueError('size of t and x must agree') 42 | if isinstance(x, np.ndarray) and isinstance(y, np.ndarray) and isinstance(t, np.ndarray) and is_mc_mode: 43 | raise ValueError('when x and y are vectors, only scalar t is allowed in MC simulations') 44 | if m < 0 or m > 4: 45 | raise ValueError('parameter m must be 0,1,2,3,4') 46 | k = init_mean_rev() 47 | G = (1.0 - np.exp(-k * (T - t))) / k 48 | bond_value = discount.df(T) / discount.df(t) * np.exp(-G * x - 0.5 * G ** 2 * y) 49 | return bond_value * np.power(-G, m) 50 | 51 | 52 | def annuity(t, ts_sw: np.ndarray, x, y, m, 53 | discount: Discount = None, 54 | is_mc_mode: bool = False): 55 | if discount is None: 56 | discount = Discount() 57 | ann = 0 58 | for i in range(1, ts_sw.size): 59 | bond_value = bond(t, ts_sw[i], x, y, m, discount=discount, is_mc_mode=is_mc_mode) 60 | ann = ann + (ts_sw[i] - ts_sw[i-1]) * bond_value 61 | return ann 62 | 63 | 64 | def swap_rate(t, ts_sw: np.ndarray, x, y, 65 | discount: Discount = None, 66 | is_mc_mode: bool = False): 67 | if discount is None: 68 | discount = Discount() 69 | if (isinstance(x, np.ndarray) and isinstance(y, float)) or (isinstance(x, float) and isinstance(y, np.ndarray)): 70 | raise ValueError('x and y both must be either scalar or vector') 71 | if isinstance(x, np.ndarray) and isinstance(y, np.ndarray): 72 | if x.shape != y.shape: 73 | raise ValueError('when x and y are vectors, they must have same shape') 74 | 75 | denumer0 = 0 76 | denumer1 = 0 77 | denumer2 = 0 78 | denumer3 = 0 79 | denumer4 = 0 80 | for i in range(1, ts_sw.size): 81 | denumer0 = denumer0 + (ts_sw[i] - ts_sw[i - 1]) * bond(t, ts_sw[i], x, y, 0, discount=discount, is_mc_mode=is_mc_mode) 82 | denumer1 = denumer1 + (ts_sw[i] - ts_sw[i - 1]) * bond(t, ts_sw[i], x, y, 1, discount=discount, is_mc_mode=is_mc_mode) 83 | denumer2 = denumer2 + (ts_sw[i] - ts_sw[i - 1]) * bond(t, ts_sw[i], x, y, 2, discount=discount, is_mc_mode=is_mc_mode) 84 | denumer3 = denumer3 + (ts_sw[i] - ts_sw[i - 1]) * bond(t, ts_sw[i], x, y, 3, discount=discount, is_mc_mode=is_mc_mode) 85 | denumer4 = denumer4 + (ts_sw[i] - ts_sw[i - 1]) * bond(t, ts_sw[i], x, y, 4, discount=discount, is_mc_mode=is_mc_mode) 86 | 87 | numer0 = bond(t, ts_sw[0], x, y, 0, discount=discount, is_mc_mode=is_mc_mode) - bond(t, ts_sw[-1], x, y, 0, discount=discount, is_mc_mode=is_mc_mode) 88 | numer1 = bond(t, ts_sw[0], x, y, 1, discount=discount, is_mc_mode=is_mc_mode) - bond(t, ts_sw[-1], x, y, 1, discount=discount, is_mc_mode=is_mc_mode) 89 | numer2 = bond(t, ts_sw[0], x, y, 2, discount=discount, is_mc_mode=is_mc_mode) - bond(t, ts_sw[-1], x, y, 2, discount=discount, is_mc_mode=is_mc_mode) 90 | numer3 = bond(t, ts_sw[0], x, y, 3, discount=discount, is_mc_mode=is_mc_mode) - bond(t, ts_sw[-1], x, y, 3, discount=discount, is_mc_mode=is_mc_mode) 91 | numer4 = bond(t, ts_sw[0], x, y, 4, discount=discount, is_mc_mode=is_mc_mode) - bond(t, ts_sw[-1], x, y, 4, discount=discount, is_mc_mode=is_mc_mode) 92 | 93 | value0 = numer0 / denumer0 94 | value1 = numer1 / denumer0 - (numer0 * denumer1) / np.power(denumer0, 2) 95 | value2 = (-2 * numer1 * denumer1) / np.power(denumer0, 2) + numer2 / denumer0 + numer0 * ((2 * np.power(denumer1, 2)) / np.power(denumer0, 3) - denumer2 / np.power(denumer0, 2)) 96 | value3 = (-3 * denumer1 * numer2) / np.power(denumer0, 2) + 3 * numer1 * ((2 * np.power(denumer1, 2)) / np.power(denumer0, 3) - denumer2 / np.power(denumer0, 2)) + numer3 / denumer0 + numer0 * ((-6 * np.power(denumer1, 3)) / np.power(denumer0, 4) + (6 * denumer1 * denumer2) / np.power(denumer0, 3) - denumer3 / np.power(denumer0, 2)) 97 | value4 = (24 * numer0 * np.power(denumer1, 4) - 12 * denumer0 * np.power(denumer1, 2) * (2 * numer1 * denumer1 + 3 * numer0 * denumer2) + 2 * np.power(denumer0, 2) * (6 * np.power(denumer1, 2) * numer2 + 3 * numer0 * np.power(denumer2, 2) + 4 * denumer1 * (3 * numer1 * denumer2 + numer0 * denumer3)) + np.power(denumer0, 4) * numer4 - np.power(denumer0, 3) * (6 * numer2 * denumer2 + 4 * denumer1 * numer3 + 4 * numer1 * denumer3 + numer0 * denumer4)) / np.power(denumer0, 5) 98 | 99 | return value0, value1, value2, value3, value4 100 | 101 | 102 | def libor_rate(t, t_start: float, t_end: float, 103 | x, y, 104 | discount: Discount = None, 105 | is_mc_mode: bool = False): 106 | if discount is None: 107 | discount = Discount() 108 | if (isinstance(x, np.ndarray) and isinstance(y, float)) or (isinstance(x, float) and isinstance(y, np.ndarray)): 109 | raise ValueError('x and y both must be either scalar or vector') 110 | if isinstance(x, np.ndarray) and isinstance(y, np.ndarray): 111 | if x.shape != y.shape: 112 | raise ValueError('when x and y are vectors, they must have same shape') 113 | 114 | zcb_start = bond(t, t_start, x, y, 0, discount=discount, is_mc_mode=is_mc_mode) 115 | zcb_end = bond(t, t_end, x, y, 0, discount=discount, is_mc_mode=is_mc_mode) 116 | libor = 1.0/(t_end - t_start) * (zcb_start/zcb_end - 1.0) 117 | 118 | return libor 119 | -------------------------------------------------------------------------------- /stochvolmodels/pricers/factor_hjm/rate_logsv_ivols.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from typing import Dict, Union 4 | from scipy.stats import norm 5 | from scipy.optimize import curve_fit, brenth 6 | 7 | ALPHA = 'alpha' 8 | BETA = 'beta' 9 | TOTAL_VOL = 'total_vol' 10 | RHO = 'rho' 11 | 12 | 13 | def get_alpha(f0: float, 14 | ttm: float, 15 | vol_atm: float, 16 | beta: float, 17 | rho: float, 18 | total_vol: float, 19 | shift: float): 20 | """ 21 | Compute SABR parameter alpha from an ATM normal volatility. 22 | Alpha is determined as the root of a 3rd degree polynomial. Return a single 23 | scalar alpha. 24 | """ 25 | f_pow_beta = np.power(f0+shift, beta) 26 | omega = -1.0 / 8 * beta * (2.0 - beta) / np.power(f0 + shift, 2.0 - 2.0 * beta) 27 | p = [1.0/3*ttm*f_pow_beta*omega, 28 | 0.0, 29 | f_pow_beta + 1.0 / 24 * ttm * f_pow_beta * total_vol ** 2 * (2.0 - 3.0 * rho ** 2), 30 | -vol_atm] 31 | 32 | roots = np.roots(p) 33 | roots_real = np.extract(np.isreal(roots), np.real(roots)) 34 | # Note: the double real roots case is not tested 35 | alpha_first_guess = vol_atm / np.power(f0+shift, beta) 36 | i_min = np.argmin(np.abs(roots_real - alpha_first_guess)) 37 | return roots_real[i_min] 38 | 39 | 40 | def calc_logsv_ivols(strikes: Union[float, np.ndarray], 41 | f0: float, 42 | ttm: float, 43 | alpha: float, 44 | rho: float, 45 | total_vol: float, 46 | beta: float, 47 | shift: float, 48 | is_alpha_atmvol: bool = False) -> Union[float, np.ndarray]: 49 | """ 50 | returns SABR normal implied volatilities 51 | """ 52 | assert f0 > 0 53 | if not np.all(strikes + shift > 0): 54 | raise ValueError('strike + shift must be positive') 55 | assert beta >= 0 and beta <= 1 56 | tol = 1e-6 57 | 58 | if is_alpha_atmvol: 59 | alpha = get_alpha(f0=f0, ttm=ttm, vol_atm=alpha, beta=beta, rho=rho, total_vol=total_vol, shift=shift) 60 | 61 | if isinstance(strikes, float): 62 | strikes = np.array([strikes]) 63 | ivols = np.zeros_like(strikes) 64 | 65 | for idx, strike in enumerate(strikes): 66 | if (1.0 - beta) >= 1e-3: # if beta is not 1 67 | zeta = total_vol / alpha * (np.power(strike + shift, 1.0 - beta) - np.power(f0 + shift, 1.0 - beta)) / (1.0 - beta) 68 | omega = -1.0 / 8 * beta * (2.0 - beta) / np.power(f0 + shift, 2.0 - 2.0 * beta) 69 | if np.fabs(strike - f0) > tol: 70 | m1 = (1.0 - beta) * (strike - f0) / (np.power(strike + shift, 1.0 - beta) - np.power(f0 + shift, 1.0 - beta)) 71 | else: 72 | m1 = np.power(f0 + shift, beta) 73 | else: # shifted lognormal case, beta = 1 74 | zeta = total_vol / alpha * np.log((strike + shift) / (f0 + shift)) 75 | omega = -1.0 / 8 76 | if np.fabs(strike - f0) > tol: 77 | m1 = (strike - f0) / np.log((strike + shift) / (f0 + shift)) 78 | else: 79 | m1 = np.power(f0 + shift, beta) 80 | y_zeta = np.log((rho + zeta + np.sqrt(1 + 2.0 * rho * zeta + zeta ** 2)) / (1.0 + rho)) 81 | e_zeta = np.sqrt(1.0 + 2.0 * rho * zeta + zeta ** 2) 82 | if np.fabs(strike - f0) > tol: 83 | theta_zeta = total_vol ** 2 / 24.0 * (-1 + 3.0 * (rho + zeta - rho * e_zeta) / (y_zeta * e_zeta)) + \ 84 | omega * alpha ** 2 / 6.0 * (1.0 - rho ** 2 + ((rho + zeta) * e_zeta - rho) / y_zeta) 85 | zeta_by_yzeta = zeta / y_zeta 86 | else: 87 | theta_zeta = total_vol ** 2 / 24.0 * (2.0 - 3.0 * rho ** 2) + omega * alpha ** 2 / 3.0 88 | zeta_by_yzeta = 1.0 89 | mult = np.where(theta_zeta >= 0.0, 1.0 + theta_zeta * ttm, 1.0 / (1.0 - theta_zeta * ttm)) 90 | ivols[idx] = alpha * m1 * zeta_by_yzeta * mult 91 | return ivols 92 | 93 | 94 | def fit_logsv_ivols(strikes: np.ndarray, 95 | mid_vols: np.ndarray, 96 | f0: float, 97 | beta: float, 98 | shift: float, 99 | ttm: float) -> Dict[str, float]: 100 | atm_fit_params = cals_logsv_parab_fit(strikes=strikes, mid_vols=mid_vols, f0=f0, beta=beta, shift=shift) 101 | bounds = ([0.001, 0.01, -0.999], [3.0*atm_fit_params[ALPHA], 5.0, 0.999]) 102 | atm_fit_params[RHO] = np.maximum(-0.99, np.minimum(0.99, atm_fit_params[RHO])) if ~np.isnan(atm_fit_params[RHO]) else 0.0 103 | atm_fit_params[TOTAL_VOL] = np.maximum(0.01, np.minimum(3.0, atm_fit_params[TOTAL_VOL])) if ~np.isnan(atm_fit_params[TOTAL_VOL]) else 0.1 104 | p0 = np.array([atm_fit_params[ALPHA], atm_fit_params[TOTAL_VOL], atm_fit_params[RHO]]) 105 | 106 | # f(x; params) 107 | ivol_func_0 = lambda log_strikes, alpha, total_vol, rho: calc_logsv_ivols(strikes=strikes, 108 | f0=f0, 109 | ttm=ttm, 110 | alpha=alpha, 111 | rho=rho, 112 | total_vol=total_vol, 113 | beta=beta, 114 | shift=shift) 115 | sigma = None 116 | popt, pcov = curve_fit(f=ivol_func_0, 117 | xdata=strikes, 118 | ydata=mid_vols, 119 | bounds=bounds, 120 | p0=p0, 121 | sigma=sigma) 122 | fit_params = {ALPHA: popt[0], BETA: beta, TOTAL_VOL: popt[1], RHO: popt[2]} 123 | return fit_params 124 | 125 | 126 | 127 | def cals_logsv_parab_fit(strikes: np.ndarray, 128 | mid_vols: np.ndarray, 129 | f0: float, 130 | beta: float, 131 | shift: float, 132 | strike_step: float = 0.001 133 | ) -> Dict[str, float]: 134 | """ 135 | compute initial fit for alpha, total_vol and rho in SABR model 136 | """ 137 | v0 = np.interp(x=f0, xp=strikes, fp=mid_vols) 138 | v0_m1 = np.interp(x=f0-strike_step, xp=strikes, fp=mid_vols) 139 | v0_p1 = np.interp(x=f0+strike_step, xp=strikes, fp=mid_vols) 140 | # derivs with respect K 141 | v1 = (v0_p1 - v0_m1) / (2.0 * strike_step) 142 | v2 = (v0_p1 - 2.0*v0 + v0_m1) / (strike_step**2) 143 | # derivs with respect to z 144 | v1 = v1 * (f0+shift) 145 | v2 = (f0+shift)**2 * v2 + v1 146 | 147 | alpha = v0 / np.power(f0+shift, beta) 148 | total_vol2 = 1.0 / np.power(f0 + shift, 2.0) * (v0**2*np.power(beta-1.0, 2.0) + 6.0*v1**2 + 6*v0*(v1-beta*v1+v2)) 149 | total_vol = np.sqrt(total_vol2) 150 | rho = (v0 - beta * v0 + 2.0 * v1) / total_vol / (f0 + shift) 151 | sabr_params = {ALPHA: alpha, BETA: beta, TOTAL_VOL: total_vol, RHO: rho} 152 | return sabr_params 153 | 154 | 155 | def get_delta_at_strikes(strikes: np.ndarray, 156 | f0: float, 157 | ttm: float, 158 | sigma0: float, 159 | rho: float, 160 | total_vol: float, 161 | beta: float, 162 | shift: float, 163 | optiontypes: np.ndarray = None 164 | ) -> pd.Series: 165 | if optiontypes is None: 166 | optiontypes = np.repeat('C', strikes.size) 167 | st = np.sqrt(ttm) 168 | moneyness = f0 - strikes 169 | vol_st = st * calc_logsv_ivols(strikes=strikes, f0=f0, ttm=ttm, alpha=sigma0, rho=rho, total_vol=total_vol, 170 | beta=beta, shift=shift) 171 | d = moneyness / vol_st 172 | deltas = np.where(optiontypes == "C", norm.cdf(d), norm.cdf(d)-1) 173 | 174 | return deltas 175 | 176 | 177 | def infer_strikes_from_deltas(deltas: np.ndarray, 178 | f0: float, 179 | ttm: float, 180 | sigma0: float, 181 | rho: float, 182 | total_vol: float, 183 | beta: float, 184 | shift: float 185 | ) -> pd.Series: 186 | """ 187 | givem 188 | """ 189 | st = np.sqrt(ttm) 190 | def func(strike: float, given_delta: float) -> float: 191 | moneyness = f0-strike 192 | vol_st = st * calc_logsv_ivols(strikes=strike, f0=f0, ttm=ttm, alpha=sigma0, rho=rho, total_vol=total_vol, 193 | beta=beta, shift=shift) 194 | if given_delta >= 0.0: 195 | target = norm.ppf(given_delta) 196 | else: 197 | target = norm.ppf(1.0+given_delta) 198 | f = moneyness / vol_st - target 199 | return f 200 | 201 | imp_deltas = {} 202 | a = -shift + 0.0001 203 | b = 20*f0 204 | for idx, given_delta in enumerate(deltas): 205 | try: 206 | strike = brenth(f=func, a=a, b=b, args=(given_delta)) # , x0=forward 207 | except: 208 | print(f"can't find strike for delta={given_delta}, ttm={ttm}, forward={f0}") 209 | strike = f0 210 | imp_deltas[given_delta] = strike 211 | 212 | imp_deltas = pd.DataFrame.from_dict(imp_deltas, orient='index') 213 | return imp_deltas.iloc[:, 0] 214 | 215 | 216 | -------------------------------------------------------------------------------- /stochvolmodels/pricers/logsv/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/StochVolModels/228ed94afd25a561f01afb5c1c7c5160454dc9f1/stochvolmodels/pricers/logsv/__init__.py -------------------------------------------------------------------------------- /stochvolmodels/pricers/logsv/logsv_params.py: -------------------------------------------------------------------------------- 1 | """ 2 | implementation of log sv params 3 | """ 4 | import numpy as np 5 | import pandas as pd 6 | from numpy import linalg as la 7 | from dataclasses import dataclass, asdict 8 | from typing import Optional, Dict, Any 9 | 10 | from stochvolmodels import VariableType, find_nearest 11 | from stochvolmodels.pricers.model_pricer import ModelParams 12 | 13 | 14 | @dataclass 15 | class LogSvParams(ModelParams): 16 | """ 17 | Implementation of model params class 18 | """ 19 | sigma0: float = 0.2 20 | theta: float = 0.2 21 | kappa1: float = 1.0 22 | kappa2: Optional[float] = 2.5 # Optional is mapped to self.kappa1 / self.theta 23 | beta: float = -1.0 24 | volvol: float = 1.0 25 | H: float = 0.5 # Hurst index 26 | vol_backbone: pd.Series = None 27 | 28 | def __post_init__(self): 29 | if self.kappa2 is None: 30 | self.kappa2 = self.kappa1 / self.theta 31 | assert -0.5 < self.H <= 0.5 32 | 33 | def to_dict(self) -> Dict[str, Any]: 34 | return asdict(self) 35 | 36 | def to_str(self) -> str: 37 | return f"sigma0={self.sigma0:0.2f}, theta={self.theta:0.2f}, kappa1={self.kappa1:0.2f}, kappa2={self.kappa2:0.2f}, " \ 38 | f"beta={self.beta:0.2f}, volvol={self.volvol:0.2f}" 39 | 40 | def set_vol_backbone(self, vol_backbone: pd.Series) -> None: 41 | self.vol_backbone = vol_backbone 42 | 43 | def get_vol_backbone_eta(self, tau: float) -> float: 44 | if self.vol_backbone is not None: 45 | nearest_tau = find_nearest(a=self.vol_backbone.index.to_numpy(), value=tau, is_equal_or_largest=True) 46 | vol_backbone_eta = self.vol_backbone.loc[nearest_tau] 47 | else: 48 | vol_backbone_eta = 1.0 49 | return vol_backbone_eta 50 | 51 | def get_vol_backbone_etas(self, ttms: np.ndarray) -> np.ndarray: 52 | if self.vol_backbone is not None: 53 | vol_backbone_etas = np.ones_like(ttms) 54 | for idx, tau in enumerate(ttms): 55 | nearest_tau = find_nearest(a=self.vol_backbone.index.to_numpy(), value=tau, is_equal_or_largest=True) 56 | vol_backbone_etas[idx] = self.vol_backbone.loc[nearest_tau] 57 | else: 58 | vol_backbone_etas = np.ones_like(ttms) 59 | return vol_backbone_etas 60 | 61 | @property 62 | def kappa(self) -> float: 63 | return self.kappa1 + self.kappa2 * self.theta 64 | 65 | @property 66 | def theta2(self) -> float: 67 | return self.theta * self.theta 68 | 69 | @property 70 | def vartheta2(self) -> float: 71 | return self.beta * self.beta + self.volvol * self.volvol 72 | 73 | @property 74 | def gamma(self) -> float: 75 | """ 76 | assume kappa2 = kappa1 / theta 77 | """ 78 | return self.kappa1 / self.theta 79 | 80 | @property 81 | def eta(self) -> float: 82 | """ 83 | assume kappa2 = kappa1 / theta 84 | """ 85 | return self.kappa1 * self.theta / self.vartheta2 - 1.0 86 | 87 | def get_x_grid(self, ttm: float = 1.0, n_stdevs: float = 3.0, n: int = 200) -> np.ndarray: 88 | """ 89 | spacial grid to compute density of x 90 | """ 91 | sigma_t = np.sqrt(ttm * 0.5 * (np.square(self.sigma0) + np.square(self.theta))) 92 | drift = - 0.5 * sigma_t * sigma_t 93 | stdev = (n_stdevs + 1) * sigma_t 94 | return np.linspace(-stdev + drift, stdev + drift, n) 95 | 96 | def get_sigma_grid(self, ttm: float = 1.0, n_stdevs: float = 3.0, n: int = 200) -> np.ndarray: 97 | """ 98 | spacial grid to compute density of sigma 99 | """ 100 | sigma_t = np.sqrt(0.5 * (np.square(self.sigma0) + np.square(self.theta))) 101 | vvol = 0.5 * np.sqrt(self.vartheta2 * ttm) 102 | return np.linspace(0.0, sigma_t + n_stdevs * vvol, n) 103 | 104 | def get_qvar_grid(self, ttm: float = 1.0, n_stdevs: float = 3.0, n: int = 200) -> np.ndarray: 105 | """ 106 | spacial grid to compute density of i 107 | """ 108 | sigma_t = np.sqrt(ttm * (np.square(self.sigma0) + np.square(self.theta))) 109 | vvol = np.sqrt(self.vartheta2) * ttm 110 | return np.linspace(0.0, sigma_t + n_stdevs * vvol, n) 111 | 112 | def get_variable_space_grid(self, variable_type: VariableType = VariableType.LOG_RETURN, 113 | ttm: float = 1.0, 114 | n_stdevs: float = 3, 115 | n: int = 200 116 | ) -> np.ndarray: 117 | if variable_type == VariableType.LOG_RETURN: 118 | return self.get_x_grid(ttm=ttm, n_stdevs=n_stdevs, n=n) 119 | if variable_type == VariableType.SIGMA: 120 | return self.get_sigma_grid(ttm=ttm, n_stdevs=n_stdevs, n=n) 121 | elif variable_type == VariableType.Q_VAR: 122 | return self.get_qvar_grid(ttm=ttm, n_stdevs=n_stdevs, n=n) 123 | else: 124 | raise NotImplementedError 125 | 126 | def get_vol_moments_lambda(self, 127 | n_terms: int = 4 128 | ) -> np.ndarray: 129 | 130 | kappa2 = self.kappa2 131 | kappa = self.kappa 132 | vartheta2 = self.vartheta2 133 | theta = self.theta 134 | theta2 = self.theta2 135 | 136 | def c(n: int) -> float: 137 | return 0.5 * vartheta2 * n * (n - 1.0) 138 | 139 | lambda_m = np.zeros((n_terms, n_terms)) 140 | lambda_m[0, 0] = -kappa 141 | lambda_m[0, 1] = -kappa2 142 | lambda_m[1, 0] = 2.0 * c(2) * theta 143 | lambda_m[1, 1] = c(2) - 2.0 * kappa 144 | lambda_m[1, 2] = -2.0 * kappa2 145 | 146 | for n_ in np.arange(2, n_terms): 147 | n = n_ + 1 # n_ is array counter, n is formula counter 148 | c_n = c(n) 149 | lambda_m[n_, n_ - 2] = c_n * theta2 150 | lambda_m[n_, n_ - 1] = 2.0 * c_n * theta 151 | lambda_m[n_, n_] = c_n - n * kappa 152 | if n_ + 1 < n_terms: 153 | lambda_m[n_, n_ + 1] = -n * kappa2 154 | 155 | return lambda_m 156 | 157 | def assert_vol_moments_stability(self, n_terms: int = 4): 158 | lambda_m = self.get_vol_moments_lambda(n_terms=n_terms) 159 | w, v = la.eig(lambda_m) 160 | cond = np.all(np.real(w) < 0.0) 161 | print(f"vol moments stable = {cond}") 162 | 163 | def print_vol_moments_stability(self, n_terms: int = 4) -> None: 164 | def c(n: int) -> float: 165 | return 0.5 * self.vartheta2 * n * (n - 1.0) 166 | 167 | cond_m2 = c(2) - 2.0 * self.kappa 168 | print(f"con2:\n{cond_m2}") 169 | cond_m3 = c(3) - 3.0 * self.kappa 170 | print(f"con3:\n{cond_m3}") 171 | cond_m4 = c(4) - 4.0 * self.kappa 172 | print(f"cond4:\n{cond_m4}") 173 | 174 | lambda_m = self.get_vol_moments_lambda(n_terms=n_terms) 175 | print(f"lambda_m:\n{lambda_m}") 176 | 177 | w, v = la.eig(lambda_m) 178 | print(f"eigenvalues w:\n{w}") 179 | print(f"vol moments stable = {np.all(np.real(w) < 0.0)}") 180 | -------------------------------------------------------------------------------- /stochvolmodels/pricers/logsv/vol_moments_ode.py: -------------------------------------------------------------------------------- 1 | """ 2 | analytics for vol and QV moments computation 3 | """ 4 | # packages 5 | import numpy as np 6 | import pandas as pd 7 | import matplotlib.pyplot as plt 8 | import seaborn as sns 9 | from numpy import linalg as la 10 | from scipy import linalg as sla 11 | from enum import Enum 12 | # project 13 | from stochvolmodels.pricers.logsv.logsv_params import LogSvParams 14 | from stochvolmodels.utils.funcs import set_seed 15 | 16 | 17 | VOLVOL = 1.75 18 | 19 | DRIFT_PARAMS = {'$(\kappa_{1}=4, \kappa_{2}=0)$': LogSvParams(sigma0=1.0, theta=1.0, kappa1=4.0, kappa2=0.0, beta=0.0, volvol=VOLVOL), 20 | '$(\kappa_{1}=4, \kappa_{2}=4)$': LogSvParams(sigma0=1.0, theta=1.0, kappa1=4.0, kappa2=4.0, beta=0.0, volvol=VOLVOL), 21 | '$(\kappa_{1}=4, \kappa_{2}=8)$': LogSvParams(sigma0=1.0, theta=1.0, kappa1=4.0, kappa2=8.0, beta=0.0, volvol=VOLVOL)} 22 | 23 | 24 | def compute_analytic_vol_moments(params: LogSvParams, 25 | t: float = 1.0, 26 | n_terms: int = 4, 27 | is_qvar: bool = False 28 | ) -> np.ndarray: 29 | 30 | lambda_m = params.get_vol_moments_lambda(n_terms=n_terms) 31 | 32 | y = params.sigma0 - params.theta 33 | y0 = np.zeros(n_terms) 34 | for n in range(0, n_terms): 35 | y0[n] = np.power(y, n+1) 36 | 37 | if np.isclose(np.abs(t), 0.0): 38 | return y0 39 | 40 | rhs = np.zeros(n_terms) 41 | rhs[1] = params.vartheta2*params.theta2 42 | 43 | if is_qvar: # need flat boundary condition 44 | rhs[-1] = -n_terms*params.kappa2*np.power(y, n_terms+1) 45 | else: 46 | rhs[-1] = -n_terms*params.kappa2*np.power(y, n_terms+1) 47 | 48 | i_m = la.inv(lambda_m) 49 | is_expm = True 50 | if is_expm: 51 | e_m = sla.expm(lambda_m*t) 52 | m_rhs = i_m @ (e_m - np.eye(n_terms)) 53 | else: 54 | w, v = la.eig(lambda_m) 55 | v_inv = la.inv(v) 56 | e_m = np.real(v @ np.diag(np.exp(w*t)) @ v_inv) 57 | m_rhs = np.real(v @ np.diag(np.reciprocal(w)*(np.exp(w*t)-np.ones(n_terms))) @ v_inv) 58 | # m_rhs = i_m @ (e_m - np.eye(n_terms)) 59 | 60 | if is_qvar: 61 | sol1 = m_rhs @ y0 62 | intm2 = i_m @ (m_rhs-t*np.eye(n_terms)) 63 | sol2 = intm2 @ rhs 64 | else: 65 | sol1 = e_m @ y0 66 | sol2 = m_rhs @ rhs 67 | 68 | sol = sol1 + sol2 69 | return sol 70 | 71 | 72 | def compute_analytic_qvar(params: LogSvParams, 73 | ttm: float = 1.0, 74 | n_terms: int = 4 75 | ) -> float: 76 | """ 77 | compute expected value [ (1/T) int^T_0 sigma^2_t dt] 78 | """ 79 | if np.isclose(ttm, 0.0): 80 | qvar = np.square(params.sigma0) 81 | else: 82 | int_moments = compute_analytic_vol_moments(params=params, t=ttm, n_terms=n_terms, is_qvar=True) 83 | qvar = (int_moments[1] + 2.0*params.theta*int_moments[0]) / ttm + params.theta2 84 | return qvar 85 | 86 | 87 | def compute_vol_moments_t(params: LogSvParams, 88 | ttm: np.ndarray, 89 | n_terms: int = 4, 90 | is_print: bool = False 91 | ) -> np.ndarray: 92 | moments = np.zeros((len(ttm), n_terms)) 93 | for idx, t_ in enumerate(ttm): 94 | moments_ = compute_analytic_vol_moments(t=t_, params=params, n_terms=n_terms) 95 | if is_print: 96 | print(f"t={t_}: {moments_}") 97 | moments[idx, :] = moments_ 98 | return moments 99 | 100 | 101 | def compute_expected_vol_t(params: LogSvParams, 102 | t: np.ndarray, 103 | n_terms: int = 4, 104 | ) -> np.ndarray: 105 | ev = np.zeros(len(t)) 106 | for idx, t_ in enumerate(t): 107 | moments = compute_analytic_vol_moments(t=t_, params=params, n_terms=n_terms) 108 | ev[idx] = moments[0] + params.theta 109 | return ev 110 | 111 | 112 | def compute_sqrt_qvar_t(params: LogSvParams, t: np.ndarray, n_terms: int = 4) -> np.ndarray: 113 | ev = np.zeros(len(t)) 114 | for idx, t_ in enumerate(t): 115 | ev[idx] = np.sqrt(compute_analytic_qvar(ttm=t_, params=params, n_terms=n_terms)) 116 | return ev 117 | 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 | 153 | class UnitTests(Enum): 154 | VOL_MOMENTS = 1 155 | EXPECTED_VOL = 2 156 | EXPECTED_QVAR = 3 157 | VOL_BACKBONE = 4 158 | 159 | 160 | def run_unit_test(unit_test: UnitTests): 161 | 162 | from stochvolmodels.pricers.logsv_pricer import LogSVPricer 163 | logsv_pricer = LogSVPricer() 164 | 165 | n_terms = 4 166 | nb_path = 200000 167 | ttm = 1.0 168 | params = LogSvParams(sigma0=1.0, theta=1.0, kappa1=4.0, kappa2=4.0, beta=0.0, volvol=1.75) 169 | params.assert_vol_moments_stability(n_terms=n_terms) 170 | set_seed(8) # 8 171 | sigma_t, grid_t = logsv_pricer.simulate_vol_paths(ttm=ttm, params=params, nb_path=nb_path) 172 | 173 | if unit_test == UnitTests.VOL_MOMENTS: 174 | 175 | mcs = [] 176 | for n in np.arange(n_terms): 177 | if n > 0: 178 | m_n = np.power(sigma_t-params.theta, n+1) 179 | else: 180 | m_n = sigma_t - params.theta 181 | mc_mean, mc_std = np.mean(m_n, axis=1), np.std(sigma_t, axis=1) / np.sqrt(nb_path) 182 | mc = pd.Series(mc_mean, index=grid_t, name=f"MC m{n+1}") 183 | mc_m = pd.Series(mc_mean-1.96*mc_std, index=grid_t, name='MC-cd') 184 | mc_p = pd.Series(mc_mean+1.96*mc_std, index=grid_t, name='MC+cd') 185 | mcs.append(mc) 186 | analytic_vol_moments = compute_vol_moments_t(params=params, ttm=grid_t, n_terms=n_terms) 187 | analytic_vol_moments = pd.DataFrame(analytic_vol_moments, index=grid_t, columns=[f"m{n+1}" for n in range(n_terms)]) 188 | mcs = pd.concat(mcs, axis=1) 189 | 190 | df = pd.concat([analytic_vol_moments, mcs], axis=1) 191 | print(df) 192 | df.plot() 193 | 194 | elif unit_test == UnitTests.EXPECTED_VOL: 195 | 196 | mc_mean, mc_std = np.mean(sigma_t, axis=1), np.std(sigma_t, axis=1) / np.sqrt(nb_path) 197 | mc = pd.Series(mc_mean, index=grid_t, name='MC') 198 | mc_m = pd.Series(mc_mean-1.96*mc_std, index=grid_t, name='MC-cd') 199 | mc_p = pd.Series(mc_mean+1.96*mc_std, index=grid_t, name='MC+cd') 200 | 201 | analytic_vol_moments = compute_expected_vol_t(params=params, t=grid_t, n_terms=n_terms) 202 | analytic_vol_moments = pd.Series(analytic_vol_moments, index=grid_t, name='Analytic') 203 | 204 | df = pd.concat([analytic_vol_moments, mc, mc_m, mc_p], axis=1) 205 | print(df) 206 | df.plot() 207 | 208 | elif unit_test == UnitTests.EXPECTED_QVAR: 209 | 210 | q_var = pd.DataFrame(np.square(sigma_t)).expanding(axis=0).mean().to_numpy() 211 | mc_mean = np.sqrt(np.mean(q_var, axis=1)) 212 | mc_std = np.std(q_var, axis=1) / np.sqrt(nb_path) 213 | mc = pd.Series(mc_mean, index=grid_t, name='MC') 214 | mc_m = pd.Series(mc_mean-1.96*mc_std, index=grid_t, name='MC-cd') 215 | mc_p = pd.Series(mc_mean+1.96*mc_std, index=grid_t, name='MC+cd') 216 | 217 | analytic_vol_moments = compute_sqrt_qvar_t(params=params, t=grid_t, n_terms=n_terms) 218 | analytic_vol_moments = pd.Series(analytic_vol_moments, index=grid_t, name='Analytic') 219 | 220 | df = pd.concat([analytic_vol_moments, mc, mc_m, mc_p], axis=1) 221 | with sns.axes_style('darkgrid'): 222 | fig, ax = plt.subplots(1, 1, figsize=(18, 10), tight_layout=True) 223 | sns.lineplot(data=analytic_vol_moments, dashes=False, ax=ax) 224 | ax.errorbar(x=df.index[::5], y=mc_mean[::5], yerr=mc_std[::5], fmt='o', color='green', capsize=8) 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 | 231 | plt.show() 232 | 233 | 234 | if __name__ == '__main__': 235 | 236 | unit_test = UnitTests.VOL_BACKBONE 237 | 238 | is_run_all_tests = False 239 | if is_run_all_tests: 240 | for unit_test in UnitTests: 241 | run_unit_test(unit_test=unit_test) 242 | else: 243 | run_unit_test(unit_test=unit_test) 244 | -------------------------------------------------------------------------------- /stochvolmodels/pricers/tdist_pricer.py: -------------------------------------------------------------------------------- 1 | """ 2 | implementation of gaussian mixture pricer and calibration 3 | """ 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | from dataclasses import dataclass 7 | from scipy.optimize import minimize 8 | from numba.typed import List 9 | from typing import Tuple 10 | from enum import Enum 11 | 12 | # sv 13 | import stochvolmodels.pricers.analytic.tdist as td 14 | from stochvolmodels.utils.funcs import to_flat_np_array, timer 15 | from stochvolmodels.pricers.model_pricer import ModelParams, ModelPricer 16 | from stochvolmodels.utils.config import VariableType 17 | 18 | # data 19 | from stochvolmodels.data.option_chain import OptionChain 20 | 21 | 22 | @dataclass 23 | class TdistParams(ModelParams): 24 | drift: float 25 | vol: float 26 | nu: float 27 | ttm: float # ttm is important as all params are fixed to this ttm, it is not part of calibration 28 | 29 | 30 | class TdistPricer(ModelPricer): 31 | 32 | def price_chain(self, option_chain: OptionChain, params: TdistParams, **kwargs) -> np.ndarray: 33 | """ 34 | implementation of generic method price_chain using heston wrapper for tdist prices 35 | """ 36 | model_prices_ttms = tdist_vanilla_chain_pricer(drift=params.drift, 37 | vol=params.vol, 38 | nu=params.nu, 39 | ttms=option_chain.ttms, 40 | forwards=option_chain.forwards, 41 | strikes_ttms=option_chain.strikes_ttms, 42 | optiontypes_ttms=option_chain.optiontypes_ttms, 43 | discfactors=option_chain.discfactors) 44 | 45 | return model_prices_ttms 46 | 47 | def model_mc_price_chain(self, option_chain: OptionChain, params: TdistParams, 48 | nb_path: int = 100000, 49 | variable_type: VariableType = VariableType.LOG_RETURN, 50 | **kwargs 51 | ) -> (List[np.ndarray], List[np.ndarray]): 52 | raise NotImplementedError 53 | 54 | @timer 55 | def calibrate_model_params_to_chain_slice(self, 56 | option_chain: OptionChain, 57 | params0: TdistParams = None, 58 | is_vega_weighted: bool = True, 59 | is_unit_ttm_vega: bool = False, 60 | **kwargs 61 | ) -> TdistParams: 62 | """ 63 | implementation of model calibration interface 64 | fit: TdistParams 65 | nb: always use option_chain with one slice because we need martingale condition per slice 66 | """ 67 | ttms = option_chain.ttms 68 | if len(ttms) > 1: 69 | raise NotImplementedError(f"cannot calibrate to multiple slices") 70 | ttm = ttms[0] 71 | rf_rate = option_chain.discount_rates[0] 72 | 73 | # p0 = (gmm_weights, gmm_mus, gmm_vols) 74 | if params0 is not None: 75 | p0 = np.array([params0.vol, params0.nu]) 76 | else: 77 | p0 = np.array([0.2, 3.0]) 78 | 79 | vol_bounds = [(0.05, 10.0)] 80 | nu_bounds = [(2.01, 20.0)] 81 | bounds = np.concatenate((vol_bounds, nu_bounds)) 82 | 83 | x, y = option_chain.get_chain_data_as_xy() 84 | market_vols = to_flat_np_array(y) # market mid quotes 85 | if is_vega_weighted: 86 | vegas_ttms = option_chain.get_chain_vegas(is_unit_ttm_vega=is_unit_ttm_vega) 87 | vegas_ttms = [vegas_ttm/sum(vegas_ttm) for vegas_ttm in vegas_ttms] 88 | weights = to_flat_np_array(vegas_ttms) 89 | else: 90 | weights = np.ones_like(market_vols) 91 | 92 | def parse_model_params(pars: np.ndarray) -> TdistParams: 93 | vol = pars[0] 94 | nu = pars[1] 95 | drift = td.imply_drift_tdist(rf_rate=rf_rate, vol=vol, nu=nu, ttm=ttm) 96 | return TdistParams(vol=vol, nu=nu, drift=drift, ttm=ttm) 97 | 98 | def objective(pars: np.ndarray, args: np.ndarray) -> float: 99 | params = parse_model_params(pars=pars) 100 | model_vols = self.compute_model_ivols_for_chain(option_chain=option_chain, params=params) 101 | resid = np.nansum(weights * np.square(to_flat_np_array(model_vols) - market_vols)) 102 | return resid 103 | 104 | options = {'disp': True, 'ftol': 1e-10, 'maxiter': 500} 105 | res = minimize(objective, p0, args=None, method='SLSQP', bounds=bounds, options=options) 106 | fit_params = parse_model_params(pars=res.x) 107 | 108 | return fit_params 109 | 110 | @timer 111 | def calibrate_model_params_to_chain(self, 112 | option_chain: OptionChain, 113 | is_vega_weighted: bool = True, 114 | is_unit_ttm_vega: bool = False, 115 | **kwargs 116 | ) -> List[str, TdistParams]: 117 | """ 118 | model params are fitted per slice 119 | need to splic chain to slices 120 | """ 121 | fit_params = {} 122 | params0 = None 123 | for ids_ in option_chain.ids: 124 | option_chain0 = OptionChain.get_slices_as_chain(option_chain, ids=[ids_]) 125 | params0 = self.calibrate_model_params_to_chain_slice(option_chain=option_chain0, 126 | params0=params0, 127 | is_vega_weighted=is_vega_weighted, 128 | is_unit_ttm_vega=is_unit_ttm_vega, 129 | **kwargs) 130 | fit_params[ids_] = params0 131 | return fit_params 132 | 133 | 134 | def tdist_vanilla_chain_pricer(vol: float, 135 | nu: float, 136 | drift: float, 137 | ttms: np.ndarray, 138 | forwards: np.ndarray, 139 | strikes_ttms: Tuple[np.ndarray, ...], 140 | optiontypes_ttms: Tuple[np.ndarray, ...], 141 | discfactors: np.ndarray, 142 | ) -> np.ndarray: 143 | """ 144 | vectorised bsm deltas for array of aligned strikes, vols, and optiontypes 145 | """ 146 | # outputs as numpy lists 147 | model_prices_ttms = List() 148 | for ttm, forward, discfactor, strikes_ttm, optiontypes_ttm in zip(ttms, forwards, discfactors, strikes_ttms, 149 | optiontypes_ttms): 150 | option_prices_ttm = td.compute_vanilla_price_tdist(spot=forward*discfactor, 151 | strikes=strikes_ttm, 152 | ttm=ttm, 153 | vol=vol, 154 | nu=nu, 155 | optiontypes=optiontypes_ttm, 156 | rf_rate=drift, 157 | is_compute_risk_neutral_mu=False # drift is already adjusted 158 | ) 159 | model_prices_ttms.append(option_prices_ttm) 160 | 161 | return model_prices_ttms 162 | 163 | 164 | class UnitTests(Enum): 165 | CALIBRATOR = 1 166 | 167 | 168 | def run_unit_test(unit_test: UnitTests): 169 | 170 | import seaborn as sns 171 | from stochvolmodels.utils import plots as plot 172 | import stochvolmodels.data.test_option_chain as chains 173 | 174 | if unit_test == UnitTests.CALIBRATOR: 175 | # option_chain = chains.get_btc_test_chain_data() 176 | option_chain = chains.get_spy_test_chain_data() 177 | # option_chain = chains.get_gld_test_chain_data() 178 | 179 | tdist_pricer = TdistPricer() 180 | fit_params = tdist_pricer.calibrate_model_params_to_chain(option_chain=option_chain) 181 | 182 | with sns.axes_style('darkgrid'): 183 | fig, axs = plt.subplots(2, 2, figsize=(14, 12), tight_layout=True) 184 | axs = plot.to_flat_list(axs) 185 | 186 | for idx, (key, params) in enumerate(fit_params.items()): 187 | print(f"{key}: {params}") 188 | option_chain0 = OptionChain.get_slices_as_chain(option_chain, ids=[key]) 189 | tdist_pricer.plot_model_ivols_vs_bid_ask(option_chain=option_chain0, params=params, axs=[axs[idx]]) 190 | 191 | plt.show() 192 | 193 | 194 | if __name__ == '__main__': 195 | 196 | unit_test = UnitTests.CALIBRATOR 197 | 198 | is_run_all_tests = False 199 | if is_run_all_tests: 200 | for unit_test in UnitTests: 201 | run_unit_test(unit_test=unit_test) 202 | else: 203 | run_unit_test(unit_test=unit_test) 204 | -------------------------------------------------------------------------------- /stochvolmodels/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/StochVolModels/228ed94afd25a561f01afb5c1c7c5160454dc9f1/stochvolmodels/tests/__init__.py -------------------------------------------------------------------------------- /stochvolmodels/tests/bsm_mgf_pricer.py: -------------------------------------------------------------------------------- 1 | """ 2 | test option valuation using moment generating function analytics for Black-Scholes-Merton model 3 | """ 4 | 5 | import numpy as np 6 | import pandas as pd 7 | import matplotlib.pyplot as plt 8 | import seaborn as sns 9 | from typing import Tuple 10 | from enum import Enum 11 | 12 | import stochvolmodels.utils.mgf_pricer as mgfp 13 | from stochvolmodels.pricers.analytic.bsm import infer_bsm_ivols_from_model_chain_prices 14 | from stochvolmodels.utils.config import VariableType 15 | 16 | 17 | def compute_normal_mgf_grid(ttm: float, 18 | vol: float, 19 | is_spot_measure: bool = True 20 | ) -> Tuple[np.ndarray, np.ndarray]: 21 | """ 22 | for normal: mgf = exp( 0.5*PHI * (PHI + 1.0)*sigma^2*T ) 23 | add two columns to make compatible with @ for numba in slice_pricer_with_a_grid 24 | """ 25 | phi_grid = mgfp.get_phi_grid(is_spot_measure=is_spot_measure) 26 | if is_spot_measure: # use 1 for spot measure 27 | alpha = 1.0 28 | else: 29 | alpha = -1.0 30 | log_mgf_grid = 0.5 * phi_grid * (phi_grid + alpha) * (ttm * vol * vol) 31 | return log_mgf_grid, phi_grid 32 | 33 | 34 | def compute_normal_mgf_psi_grid(ttm: float, 35 | vol: float, 36 | is_spot_measure: bool = True 37 | ) -> Tuple[np.ndarray, np.ndarray]: 38 | psi_grid = mgfp.get_psi_grid(is_spot_measure=is_spot_measure) 39 | log_mgf_grid = -psi_grid * (ttm * vol * vol) 40 | return log_mgf_grid, psi_grid 41 | 42 | 43 | def bsm_slice_pricer(ttm: float, 44 | forward: float, 45 | vol: float, 46 | strikes: np.ndarray, 47 | optiontypes: np.ndarray, 48 | variable_type: VariableType = VariableType.LOG_RETURN, 49 | is_spot_measure: bool = True 50 | ) -> Tuple[np.ndarray, np.ndarray]: 51 | 52 | if variable_type == VariableType.LOG_RETURN: 53 | log_mgf_grid, phi_grid = compute_normal_mgf_grid(ttm=ttm, vol=vol, is_spot_measure=is_spot_measure) 54 | bsm_prices = mgfp.vanilla_slice_pricer_with_mgf_grid(log_mgf_grid=log_mgf_grid, 55 | phi_grid=phi_grid, 56 | forward=forward, 57 | strikes=strikes, 58 | optiontypes=optiontypes, 59 | is_spot_measure=is_spot_measure) 60 | bsm_ivols = infer_bsm_ivols_from_model_chain_prices(ttms=np.array([ttm]), 61 | forwards=np.array([forward]), 62 | discfactors=np.array([1.0]), 63 | strikes_ttms=(strikes,), 64 | optiontypes_ttms=(optiontypes,), 65 | model_prices_ttms=(bsm_prices,)) 66 | elif variable_type == VariableType.Q_VAR: 67 | log_mgf_grid, psi_grid = compute_normal_mgf_psi_grid(ttm=ttm, vol=vol, is_spot_measure=is_spot_measure) 68 | bsm_prices = mgfp.slice_qvar_pricer_with_a_grid(log_mgf_grid=log_mgf_grid, 69 | psi_grid=psi_grid, 70 | ttm=ttm, 71 | strikes=strikes, 72 | optiontypes=optiontypes, 73 | is_spot_measure=is_spot_measure, 74 | forward=forward) 75 | bsm_ivols = np.zeros_like(bsm_prices) 76 | else: 77 | raise ValueError(f"not implemented") 78 | 79 | return bsm_prices, bsm_ivols 80 | 81 | 82 | def compare_spot_and_inverse_options(): 83 | ttm = 1.0 84 | forward = 1.0 85 | vol = 1.0 86 | strikes = np.linspace(0.5, 5.0, 19) 87 | optiontypes = np.full(strikes.shape, 'C') 88 | optiontypes_inverse = np.full(strikes.shape, 'IC') 89 | 90 | # spot 91 | bsm_prices_spot, bsm_ivols = bsm_slice_pricer(ttm=ttm, forward=forward, vol=vol, strikes=strikes, 92 | optiontypes=optiontypes, 93 | is_spot_measure=True) 94 | bsm_prices_inverse, bsm_ivols = bsm_slice_pricer(ttm=ttm, forward=forward, vol=vol, strikes=strikes, 95 | optiontypes=optiontypes_inverse, 96 | is_spot_measure=False) 97 | 98 | bsm_prices_spot = pd.Series(bsm_prices_spot, index=strikes, name='spot') 99 | bsm_prices_inverse = pd.Series(bsm_prices_inverse, index=strikes, name='inverse') 100 | 101 | prices = pd.concat([bsm_prices_spot, bsm_prices_inverse], axis=1) 102 | with sns.axes_style("darkgrid"): 103 | fig, ax = plt.subplots(1, 1, figsize=(10, 4.0), tight_layout=True) 104 | sns.lineplot(data=prices, ax=ax) 105 | 106 | 107 | def compare_spot_and_inverse_qvar_options(): 108 | ttm = 1.0 109 | forward = 1.0 110 | vol = 1.0 111 | strikes = np.linspace(0.5, 5.0, 19) 112 | optiontypes = np.full(strikes.shape, 'C') 113 | optiontypes_inverse = np.full(strikes.shape, 'IC') 114 | 115 | # spot 116 | bsm_prices_spot, bsm_ivols = bsm_slice_pricer(ttm=ttm, forward=forward, vol=vol, strikes=strikes, 117 | optiontypes=optiontypes, 118 | variable_type=VariableType.Q_VAR, 119 | is_spot_measure=True) 120 | bsm_prices_inverse, bsm_ivols = bsm_slice_pricer(ttm=ttm, forward=forward, vol=vol, strikes=strikes, 121 | optiontypes=optiontypes_inverse, 122 | variable_type=VariableType.Q_VAR, 123 | is_spot_measure=False) 124 | 125 | bsm_prices_spot = pd.Series(bsm_prices_spot, index=strikes, name='spot') 126 | bsm_prices_inverse = pd.Series(bsm_prices_inverse, index=strikes, name='inverse') 127 | 128 | prices = pd.concat([bsm_prices_spot, bsm_prices_inverse], axis=1) 129 | with sns.axes_style("darkgrid"): 130 | fig, ax = plt.subplots(1, 1, figsize=(10, 4.0), tight_layout=True) 131 | sns.lineplot(data=prices, ax=ax) 132 | 133 | 134 | class UnitTests(Enum): 135 | BSM_SLICE_PRICER = 1 136 | SPOT_INVERSE_COMP = 2 137 | SPOT_INVERSE_QVAR_COMP = 3 138 | 139 | 140 | def run_unit_test(unit_test: UnitTests): 141 | 142 | if unit_test == UnitTests.BSM_SLICE_PRICER: 143 | ttm = 1.0 144 | forward = 1.0 145 | vol = 1.0 146 | strikes = np.linspace(0.5, 5.0, 19) 147 | optiontypes = np.full(strikes.shape, 'C') 148 | bsm_prices, bsm_ivols = bsm_slice_pricer(ttm=ttm, forward=forward, vol=vol, strikes=strikes, optiontypes=optiontypes) 149 | print(bsm_prices) 150 | print(bsm_ivols) 151 | 152 | elif unit_test == UnitTests.SPOT_INVERSE_COMP: 153 | compare_spot_and_inverse_options() 154 | 155 | elif unit_test == UnitTests.SPOT_INVERSE_QVAR_COMP: 156 | compare_spot_and_inverse_qvar_options() 157 | 158 | plt.show() 159 | 160 | 161 | if __name__ == '__main__': 162 | 163 | unit_test = UnitTests.BSM_SLICE_PRICER 164 | 165 | is_run_all_tests = False 166 | if is_run_all_tests: 167 | for unit_test in UnitTests: 168 | run_unit_test(unit_test=unit_test) 169 | else: 170 | run_unit_test(unit_test=unit_test) 171 | -------------------------------------------------------------------------------- /stochvolmodels/tests/qv_pricer.py: -------------------------------------------------------------------------------- 1 | """ 2 | compute quadratic variance 3 | """ 4 | 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | from enum import Enum 8 | 9 | import stochvolmodels.utils.mgf_pricer as mgfp 10 | from stochvolmodels.pricers.logsv import affine_expansion as afe 11 | from stochvolmodels.utils.config import VariableType 12 | from stochvolmodels.pricers.logsv_pricer import LogSVPricer 13 | from stochvolmodels import LogSvParams 14 | from stochvolmodels.pricers.logsv.vol_moments_ode import compute_analytic_qvar 15 | from stochvolmodels.pricers.analytic.bsm import infer_bsm_ivols_from_model_chain_prices 16 | from stochvolmodels.utils.funcs import set_seed 17 | from stochvolmodels.data import test_option_chain as chains 18 | from stochvolmodels.data.option_chain import OptionChain 19 | 20 | 21 | BTC_PARAMS = LogSvParams(sigma0=0.8327, theta=1.0139, kappa1=4.8606, kappa2=4.7938, beta=0.1985, volvol=2.3690) 22 | VIX_PARAMS = LogSvParams(sigma0=0.9778, theta=0.5573, kappa1=4.8360, kappa2=8.6780, beta=2.3128, volvol=1.0484) 23 | GLD_PARAMS = LogSvParams(sigma0=0.1530, theta=0.1960, kappa1=2.2068, kappa2=11.2584, beta=0.1580, volvol=2.8022) 24 | SQQQ_PARAMS = LogSvParams(sigma0=0.9259, theta=0.9166, kappa1=3.6114, kappa2=3.9401, beta=1.1902, volvol=0.6133) 25 | SPY_PARAMS = LogSvParams(sigma0=0.2297, theta=0.2692, kappa1=2.6949, kappa2=10.0107, beta=-1.5082, volvol=0.8503) 26 | # BSM_PARAMS = LogSvParams(sigma0=1.0, theta=1.0, kappa1=0.0, kappa2=0.0, beta=0.0, volvol=0.0) 27 | 28 | 29 | class UnitTests(Enum): 30 | QV_SLICE_PRICER = 1 31 | COMPARE_WITH_MC = 2 32 | 33 | 34 | def run_unit_test(unit_test: UnitTests): 35 | 36 | logsv_pricer = LogSVPricer() 37 | params = BTC_PARAMS 38 | 39 | strikes = np.linspace(0.9, 2.0, 19) 40 | optiontypes = np.full(strikes.shape, 'C') 41 | expansion_order = afe.ExpansionOrder.SECOND 42 | variable_type = VariableType.Q_VAR 43 | phi_grid, psi_grid, theta_grid = mgfp.get_transform_var_grid(variable_type=variable_type, is_spot_measure=True) 44 | 45 | if unit_test == UnitTests.QV_SLICE_PRICER: 46 | ttm = 1.0 47 | forward = compute_analytic_qvar(params=params, ttm=ttm) 48 | print(forward) 49 | a_t1, log_mgf_grid = afe.compute_logsv_a_mgf_grid(phi_grid=phi_grid, 50 | psi_grid=psi_grid, 51 | theta_grid=theta_grid, 52 | ttm=ttm, 53 | params=params, 54 | is_analytic=False, 55 | expansion_order=expansion_order, 56 | is_stiff_solver=False, 57 | **params.to_dict() 58 | ) 59 | qvar_options = mgfp.slice_qvar_pricer_with_a_grid(log_mgf_grid=log_mgf_grid, 60 | psi_grid=psi_grid, 61 | ttm=ttm, 62 | forward=forward, 63 | strikes=strikes, 64 | optiontypes=optiontypes) 65 | 66 | bsm_ivols = infer_bsm_ivols_from_model_chain_prices(ttms=np.array([ttm]), 67 | forwards=np.array([forward]), 68 | discfactors=np.array([1.0]), 69 | strikes_ttms=(strikes,), 70 | optiontypes_ttms=(optiontypes,), 71 | model_prices_ttms=(qvar_options,)) 72 | 73 | print(qvar_options) 74 | print(bsm_ivols) 75 | 76 | elif unit_test == UnitTests.COMPARE_WITH_MC: 77 | set_seed(24) # 17 78 | option_chain = chains.get_qv_options_test_chain_data() 79 | option_chain = OptionChain.get_slices_as_chain(option_chain, ids=['1m', '6m']) 80 | fig = logsv_pricer.plot_comp_mma_inverse_options_with_mc(option_chain=option_chain, 81 | params=params, 82 | is_log_strike_xaxis=False, 83 | variable_type=VariableType.Q_VAR, 84 | is_plot_vols=False, 85 | nb_path=400000) 86 | 87 | # fig.savefig("..//..//docs//figures//model_vs_mc_qvar_logsv.pdf") 88 | 89 | plt.show() 90 | 91 | 92 | if __name__ == '__main__': 93 | 94 | unit_test = UnitTests.QV_SLICE_PRICER 95 | 96 | is_run_all_tests = False 97 | if is_run_all_tests: 98 | for unit_test in UnitTests: 99 | run_unit_test(unit_test=unit_test) 100 | else: 101 | run_unit_test(unit_test=unit_test) 102 | -------------------------------------------------------------------------------- /stochvolmodels/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/StochVolModels/228ed94afd25a561f01afb5c1c7c5160454dc9f1/stochvolmodels/utils/__init__.py -------------------------------------------------------------------------------- /stochvolmodels/utils/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | config file 3 | """ 4 | 5 | from enum import Enum 6 | 7 | 8 | class VariableType(Enum): 9 | """ 10 | state variables for log SV model 11 | """ 12 | LOG_RETURN = 1 # with transform var PHI 13 | Q_VAR = 2 # with transform var PSI 14 | SIGMA = 3 # with trasform for THETA 15 | -------------------------------------------------------------------------------- /stochvolmodels/utils/funcs.py: -------------------------------------------------------------------------------- 1 | """ 2 | utility functions 3 | """ 4 | import functools 5 | import time 6 | import numpy as np 7 | import pandas as pd 8 | from numba import njit 9 | from numba.typed import List 10 | from typing import Tuple, Dict, Any, Optional, Union 11 | 12 | 13 | def to_flat_np_array(input_list: List[np.ndarray]) -> np.ndarray: 14 | return np.concatenate(input_list).ravel() 15 | 16 | 17 | @njit(cache=False, fastmath=False) 18 | def set_time_grid(ttm: float, nb_steps_per_year: int = 360) -> Tuple[int, float, np.ndarray]: 19 | """ 20 | set daily steps 21 | """ 22 | dt = 1.0 / nb_steps_per_year 23 | grid_t = np.arange(0.0, ttm+1e-6, dt) 24 | nb_steps = int(ttm * nb_steps_per_year) 25 | 26 | return nb_steps, dt, grid_t 27 | 28 | 29 | @njit(cache=False, fastmath=True) 30 | def set_seed(value): 31 | """ 32 | set seed for numba space 33 | """ 34 | np.random.seed(value) 35 | 36 | 37 | def timer(func): 38 | """Print the runtime of the decorated function""" 39 | 40 | @functools.wraps(func) 41 | def wrapper_timer(*args, **kwargs): 42 | start_time = time.perf_counter() # 1 43 | value = func(*args, **kwargs) 44 | end_time = time.perf_counter() # 2 45 | run_time = end_time - start_time # 3 46 | print(f"Finished {func.__name__!r} in {run_time:.4f} secs") 47 | return value 48 | 49 | return wrapper_timer 50 | 51 | 52 | def compute_histogram_data(data: np.ndarray, 53 | x_grid: np.ndarray, 54 | name: str = 'Histogram' 55 | ) -> pd.Series: 56 | """ 57 | compute histogram on defined discrete grid 58 | """ 59 | hist_data, bin_edges = np.histogram(a=data, 60 | bins=len(x_grid) - 1, 61 | range=(x_grid[0], x_grid[-1])) 62 | hist_data = np.append(np.array(x_grid[0]), hist_data) 63 | hist_data = hist_data / len(data) 64 | hist_data = pd.Series(hist_data, index=bin_edges, name=name) 65 | return hist_data 66 | 67 | 68 | def update_kwargs(kwargs: Dict[Any, Any], 69 | new_kwargs: Optional[Dict[Any, Any]] 70 | ) -> Dict[Any, Any]: 71 | """ 72 | update kwargs with optional kwargs dicts 73 | """ 74 | local_kwargs = kwargs.copy() 75 | if new_kwargs is not None and not len(new_kwargs) == 0: 76 | local_kwargs.update(new_kwargs) 77 | return local_kwargs 78 | 79 | 80 | @njit(cache=False, fastmath=True) 81 | def erfcc(x: Union[float, np.ndarray]) -> Union[float, np.ndarray]: 82 | """ 83 | Complementary error function. can be vectorized 84 | """ 85 | z = np.abs(x) 86 | t = 1. / (1. + 0.5 * z) 87 | r = t * np.exp( 88 | -z * z - 1.26551223 + t * (1.00002368 + t * (0.37409196 + t * (0.09678418 + t * (-0.18628806 + t * (0.27886807 + 89 | t * ( 90 | -1.13520398 + t * ( 91 | 1.48851587 + t * ( 92 | -.82215223 + t * 0.17087277))))))))) 93 | fcc = np.where(np.greater(x, 0.0), r, 2.0 - r) 94 | return fcc 95 | 96 | 97 | @njit(cache=False, fastmath=True) 98 | def ncdf(x: Union[float, np.ndarray]) -> Union[float, np.ndarray]: 99 | return 1. - 0.5 * erfcc(x / (np.sqrt(2.0))) 100 | 101 | 102 | @njit(cache=False, fastmath=True) 103 | def npdf(x: Union[float, np.ndarray], mu: float = 0.0, vol: float = 1.0) -> Union[float, np.ndarray]: 104 | return np.exp(-0.5 * np.square((x - mu) / vol)) / (vol * np.sqrt(2.0 * np.pi)) 105 | 106 | 107 | def find_nearest(a: np.ndarray, 108 | value: float, 109 | is_sorted: bool = True, 110 | is_equal_or_largest: bool = False 111 | ) -> float: 112 | """ 113 | find closest element 114 | https://stackoverflow.com/questions/2566412/find-nearest-value-in-numpy-array 115 | """ 116 | if is_sorted: 117 | idx = np.searchsorted(a, value, side="left") 118 | if is_equal_or_largest: # return the equal or largest element 119 | return a[idx] 120 | else: 121 | if idx > 0 and (idx == len(a) or np.abs(value - a[idx - 1]) < np.abs(value - a[idx])): 122 | return a[idx - 1] 123 | else: 124 | return a[idx] 125 | else: 126 | a = np.asarray(a) 127 | idx = (np.abs(a - value)).argmin() 128 | return a[idx] 129 | 130 | 131 | @njit(cache=False, fastmath=False) 132 | def pos(vec: np.ndarray) -> np.ndarray: 133 | return np.maximum(vec, 0.0) 134 | -------------------------------------------------------------------------------- /stochvolmodels/utils/mc_payoffs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Montecarlo analytics for option pay-off computations 3 | """ 4 | 5 | import numpy as np 6 | from numba import njit 7 | from stochvolmodels.utils.config import VariableType 8 | 9 | 10 | @njit(cache=False, fastmath=True) 11 | def compute_mc_vars_payoff(x0: np.ndarray, 12 | sigma0: np.ndarray, 13 | qvar0: np.ndarray, 14 | ttm: float, 15 | forward: float, 16 | strikes_ttm: np.ndarray, 17 | optiontypes_ttm: np.ndarray, 18 | discfactor: float = 1.0, 19 | variable_type: VariableType = VariableType.LOG_RETURN 20 | ) -> (np.ndarray, np.ndarray): 21 | 22 | # need to remember it for options on QVAR 23 | spots_t = forward*np.exp(x0) 24 | correnction = np.nanmean(spots_t) - forward 25 | spots_t = spots_t - correnction 26 | 27 | if variable_type == VariableType.LOG_RETURN: 28 | underlying_t = spots_t 29 | elif variable_type == VariableType.Q_VAR: 30 | underlying_t = qvar0 / ttm 31 | else: 32 | raise NotImplementedError 33 | 34 | option_prices = np.zeros_like(strikes_ttm) 35 | option_std = np.zeros_like(strikes_ttm) 36 | for idx, (strike, type_) in enumerate(zip(strikes_ttm, optiontypes_ttm)): 37 | if type_ == 'C': 38 | payoff = np.where(np.greater(underlying_t, strike), underlying_t-strike, 0.0) 39 | elif type_ == 'IC': 40 | payoff = np.where(np.greater(underlying_t, strike), underlying_t - strike, 0.0) / spots_t 41 | elif type_ == 'P': 42 | payoff = np.where(np.less(underlying_t, strike), strike-underlying_t, 0.0) 43 | elif type_ == 'IP': 44 | payoff = np.where(np.less(underlying_t, strike), strike - underlying_t, 0.0) / spots_t 45 | else: 46 | payoff = np.zeros_like(underlying_t) 47 | option_prices[idx] = discfactor*np.nanmean(payoff) 48 | option_std[idx] = discfactor*np.nanstd(payoff) 49 | 50 | return option_prices, option_std/np.sqrt(x0.shape[0]) 51 | -------------------------------------------------------------------------------- /stochvolmodels/utils/var_swap_pricer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | 4 | 5 | def compute_var_swap_strike(puts: pd.Series, calls: pd.Series, forward: float, ttm: float) -> float: 6 | joint_slice = pd.concat([puts.rename('puts'), calls.rename('calls')], axis=1).sort_index() 7 | strikes = joint_slice.index.to_numpy() 8 | otm = strikes < forward 9 | # dk = strikes[1:] - strikes[:-1] 10 | n = strikes.shape[0] 11 | dk = np.zeros(n) 12 | for idx in np.arange(n): 13 | if idx == 0: 14 | dk[idx] = strikes[1] - strikes[0] 15 | elif idx == n - 1: 16 | dk[idx] = strikes[idx] - strikes[idx-1] 17 | else: 18 | dk[idx] = 0.5*(strikes[idx+1] - strikes[idx-1]) 19 | 20 | option_strip = np.where(otm, joint_slice['puts'].to_numpy(), joint_slice['calls'].to_numpy()) 21 | var_swap_strike = 2.0 * np.nansum(dk*option_strip/np.square(strikes)) 22 | atm_strike = strikes[otm == False][0] 23 | correction = np.square(forward/atm_strike - 1.0) 24 | var_swap_strike = (var_swap_strike-correction) / ttm 25 | var_swap_strike = np.sqrt(var_swap_strike) 26 | return var_swap_strike 27 | -------------------------------------------------------------------------------- /volatility_book/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/StochVolModels/228ed94afd25a561f01afb5c1c7c5160454dc9f1/volatility_book/__init__.py -------------------------------------------------------------------------------- /volatility_book/ch_lognormal_sv/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/StochVolModels/228ed94afd25a561f01afb5c1c7c5160454dc9f1/volatility_book/ch_lognormal_sv/__init__.py -------------------------------------------------------------------------------- /volatility_book/ch_lognormal_sv/quadratic_var.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from enum import Enum 4 | 5 | # chain 6 | 7 | # analytics 8 | from stochvolmodels import LogSvParams, LogSVPricer 9 | from stochvolmodels.pricers.logsv.vol_moments_ode import compute_analytic_qvar 10 | 11 | # implementations for paper 12 | 13 | LOGSV_BTC_PARAMS = LogSvParams(sigma0=1.0, theta=1.0, kappa1=3.1844, kappa2=3.058, beta=0.1514, volvol=1.8458) 14 | 15 | 16 | def plot_qvar_figure(params: LogSvParams): 17 | 18 | logsv_pricer = LogSVPricer() 19 | 20 | # ttms = {'1m': 1.0/12.0, '6m': 0.5} 21 | ttms = {'1w': 7.0/365.0, '2w': 14.0/365.0, '1m': 1.0/12.0} 22 | 23 | forwards = np.array([compute_analytic_qvar(params=params, ttm=ttm, n_terms=4) for ttm in ttms.values()]) 24 | print(f"QV forwards = {forwards}") 25 | 26 | 27 | class UnitTests(Enum): 28 | QVAR = 1 29 | 30 | 31 | def run_unit_test(unit_test: UnitTests): 32 | 33 | if unit_test == UnitTests.QVAR: 34 | plot_qvar_figure(params=LOGSV_BTC_PARAMS) 35 | 36 | plt.show() 37 | 38 | 39 | if __name__ == '__main__': 40 | 41 | unit_test = UnitTests.QVAR 42 | 43 | is_run_all_tests = False 44 | if is_run_all_tests: 45 | for unit_test in UnitTests: 46 | run_unit_test(unit_test=unit_test) 47 | else: 48 | run_unit_test(unit_test=unit_test) 49 | --------------------------------------------------------------------------------