sdevpy 1.0.8__tar.gz → 1.0.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sdevpy-1.0.8 → sdevpy-1.0.9}/PKG-INFO +1 -1
- {sdevpy-1.0.8 → sdevpy-1.0.9}/pyproject.toml +12 -1
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/analytics/bachelier.py +8 -17
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/analytics/black.py +11 -22
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/optimization.py +2 -1
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_analytics.py +15 -1
- sdevpy-1.0.9/sdevpy/tests/test_cointegration.py +96 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_impliedvol.py +49 -8
- sdevpy-1.0.9/sdevpy/tests/test_localvol.py +318 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/timeseries/backtesting.py +2 -1
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/timeseries/cointegration.py +11 -41
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/timeseries/meanreversion.py +11 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/timeseries/timeseriestools.py +11 -10
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/tools.py +3 -0
- sdevpy-1.0.8/sdevpy/volatility/impliedvol/zerosurface.py → sdevpy-1.0.9/sdevpy/volatility/impliedvol/impliedvol.py +93 -80
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/impliedvol_calib.py +7 -5
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/biexp.py +3 -3
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/cubicvol.py +5 -3
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/logmix.py +13 -7
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/svi.py +4 -3
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/tssvi1.py +8 -5
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/tssvi2.py +7 -2
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/vsvi.py +3 -3
- sdevpy-1.0.9/sdevpy/volatility/localvol/dupire.py +197 -0
- sdevpy-1.0.9/sdevpy/volatility/localvol/localvol.py +192 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/localvol/localvol_calib.py +6 -2
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy.egg-info/PKG-INFO +1 -1
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy.egg-info/SOURCES.txt +2 -54
- sdevpy-1.0.8/sdevpy/cointegration/back_testing.py +0 -224
- sdevpy-1.0.8/sdevpy/cointegration/coint_trading.py +0 -677
- sdevpy-1.0.8/sdevpy/cointegration/data_io.py +0 -14
- sdevpy-1.0.8/sdevpy/cointegration/mean_reversion.py +0 -268
- sdevpy-1.0.8/sdevpy/cointegration/model_settings.py +0 -34
- sdevpy-1.0.8/sdevpy/cointegration/utils.py +0 -132
- sdevpy-1.0.8/sdevpy/tests/test_localvol.py +0 -0
- sdevpy-1.0.8/sdevpy/thirdparty/py_lets_be_rational/__init__.py +0 -49
- sdevpy-1.0.8/sdevpy/thirdparty/py_lets_be_rational/constants.py +0 -65
- sdevpy-1.0.8/sdevpy/thirdparty/py_lets_be_rational/erf_cody.py +0 -448
- sdevpy-1.0.8/sdevpy/thirdparty/py_lets_be_rational/exceptions.py +0 -68
- sdevpy-1.0.8/sdevpy/thirdparty/py_lets_be_rational/lets_be_rational.py +0 -801
- sdevpy-1.0.8/sdevpy/thirdparty/py_lets_be_rational/normaldistribution.py +0 -194
- sdevpy-1.0.8/sdevpy/thirdparty/py_lets_be_rational/numba_helper.py +0 -13
- sdevpy-1.0.8/sdevpy/thirdparty/py_lets_be_rational/rationalcubic.py +0 -272
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/__init__.py +0 -32
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black/__init__.py +0 -180
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black/greeks/analytical.py +0 -273
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black/greeks/numerical.py +0 -251
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black/implied_volatility.py +0 -292
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black_scholes/__init__.py +0 -84
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black_scholes/greeks/analytical.py +0 -278
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black_scholes/greeks/numerical.py +0 -318
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black_scholes/implied_volatility.py +0 -102
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black_scholes_merton/__init__.py +0 -90
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black_scholes_merton/greeks/analytical.py +0 -309
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black_scholes_merton/greeks/numerical.py +0 -266
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black_scholes_merton/implied_volatility.py +0 -116
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/helpers/__init__.py +0 -114
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/helpers/constants.py +0 -52
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/helpers/distributions.py +0 -226
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/helpers/doctest_helper.py +0 -62
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/helpers/exceptions.py +0 -59
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/helpers/numerical_greeks.py +0 -217
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/__init__.py +0 -32
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black/__init__.py +0 -236
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black/greeks/analytical.py +0 -277
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black/greeks/numerical.py +0 -222
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black/implied_volatility.py +0 -117
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black_scholes/__init__.py +0 -157
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black_scholes/greeks/analytical.py +0 -279
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black_scholes/greeks/numerical.py +0 -290
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black_scholes/implied_volatility.py +0 -110
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black_scholes_merton/__init__.py +0 -231
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black_scholes_merton/greeks/analytical.py +0 -311
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black_scholes_merton/greeks/numerical.py +0 -223
- sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black_scholes_merton/implied_volatility.py +0 -104
- sdevpy-1.0.8/sdevpy/tree/__init__.py +0 -0
- sdevpy-1.0.8/sdevpy/utilities/__init__.py +0 -0
- sdevpy-1.0.8/sdevpy/volatility/__init__.py +0 -0
- sdevpy-1.0.8/sdevpy/volatility/impliedvol/__init__.py +0 -0
- sdevpy-1.0.8/sdevpy/volatility/impliedvol/impliedvol.py +0 -45
- sdevpy-1.0.8/sdevpy/volatility/impliedvol/models/__init__.py +0 -0
- sdevpy-1.0.8/sdevpy/volatility/localvol/__init__.py +0 -0
- sdevpy-1.0.8/sdevpy/volatility/localvol/localvol.py +0 -92
- sdevpy-1.0.8/sdevpy/volatility/mlsurfacegen/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/README.md +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/analytics/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/analytics/americantree.py +0 -0
- {sdevpy-1.0.8/sdevpy/cointegration → sdevpy-1.0.9/sdevpy/machinelearning}/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/datasets.py +0 -0
- {sdevpy-1.0.8/sdevpy/machinelearning → sdevpy-1.0.9/sdevpy/machinelearning/keras}/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/keras/callbacks.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/keras/learningmodel.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/keras/learningschedules.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/keras/topology.py +0 -0
- {sdevpy-1.0.8/sdevpy/machinelearning/keras → sdevpy-1.0.9/sdevpy/machinelearning/llms}/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/llms/attention.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/llms/chat.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/llms/datasets.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/llms/gpt.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/llms/instructions.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/llms/modelconverter.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/llms/textgen.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/llms/tokenizers.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/llms/training.py +0 -0
- {sdevpy-1.0.8/sdevpy/machinelearning/llms → sdevpy-1.0.9/sdevpy/market}/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/market/correlations.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/market/eqforward.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/market/eqvolsurface.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/market/fixings.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/market/spot.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/market/yieldcurve.py +0 -0
- {sdevpy-1.0.8/sdevpy/market → sdevpy-1.0.9/sdevpy/maths}/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/constants.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/integration.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/interpolation.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/metrics.py +0 -0
- {sdevpy-1.0.8/sdevpy/maths → sdevpy-1.0.9/sdevpy/maths/rand}/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/rand/correlations.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/rand/pathconstruction.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/rand/rng.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/regression.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/sets.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/specialfunctions.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/tridiag.py +0 -0
- {sdevpy-1.0.8/sdevpy/maths/rand → sdevpy-1.0.9/sdevpy/models}/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/models/assetmodels.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/models/multiasset_heston.py +0 -0
- {sdevpy-1.0.8/sdevpy/models → sdevpy-1.0.9/sdevpy/montecarlo}/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/montecarlo/mcpricer.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/montecarlo/mcrun.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/montecarlo/pathgenerator.py +0 -0
- {sdevpy-1.0.8/sdevpy/montecarlo → sdevpy-1.0.9/sdevpy/montecarlo/payoffs}/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/montecarlo/payoffs/basic.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/montecarlo/payoffs/cashflows.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/montecarlo/payoffs/exotics.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/montecarlo/payoffs/vanillas.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/montecarlo/smoothers.py +0 -0
- {sdevpy-1.0.8/sdevpy/montecarlo/payoffs → sdevpy-1.0.9/sdevpy/pde}/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/pde/forwardpde.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/pde/pdeschemes.py +0 -0
- {sdevpy-1.0.8/sdevpy/pde → sdevpy-1.0.9/sdevpy/projects}/__init__.py +0 -0
- {sdevpy-1.0.8/sdevpy/projects → sdevpy-1.0.9/sdevpy/projects/aad}/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/aad/aad_mc.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/aad/aad_mc_nd.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/chat_gpt2.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/datafiles.py +0 -0
- {sdevpy-1.0.8/sdevpy/projects/aad → sdevpy-1.0.9/sdevpy/projects/raschka}/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/raschka/ch2_working_with_text.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/raschka/ch3_coding_attention.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/raschka/ch4_gpt_model.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/raschka/ch5_loadgpt2.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/raschka/ch5_pretraining.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/raschka/ch7_instruction_finetuning.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/raschka/raschka_datasetloader.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/raschka/raschka_dnn.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/raschka/raschka_gpt_download.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/set_limits.py +0 -0
- {sdevpy-1.0.8/sdevpy/projects/raschka → sdevpy-1.0.9/sdevpy/projects/stovol}/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/stovol/stovolgen.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/stovol/stovolplot.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/stovol/stovoltrain.py +0 -0
- {sdevpy-1.0.8/sdevpy/projects/stovol → sdevpy-1.0.9/sdevpy/projects/stovolinverse}/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/stovolinverse/stovolinvgen.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/stovolinverse/stovolinvtrain.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/update_db.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/settings.py +0 -0
- {sdevpy-1.0.8/sdevpy/projects/stovolinverse → sdevpy-1.0.9/sdevpy/tensorflow}/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tensorflow/tf_black.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tensorflow/tf_metrics.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_algos.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_dates.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_interpolation.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_marketdata.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_mc.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_pde.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_timegrids.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_utils.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_yieldcurves.py +0 -0
- {sdevpy-1.0.8/sdevpy/tensorflow → sdevpy-1.0.9/sdevpy/thirdparty}/__init__.py +0 -0
- {sdevpy-1.0.8/sdevpy/thirdparty → sdevpy-1.0.9/sdevpy/timeseries}/__init__.py +0 -0
- {sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black/greeks → sdevpy-1.0.9/sdevpy/tree}/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tree/trees.py +0 -0
- {sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black_scholes/greeks → sdevpy-1.0.9/sdevpy/utilities}/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/algos.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/book.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/clipboard.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/constants.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/dates.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/filemanager.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/jsonmanager.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/network.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/pydotnet.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/scalendar.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/speriods.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/timegrids.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/timer.py +0 -0
- {sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black_scholes_merton/greeks → sdevpy-1.0.9/sdevpy/volatility}/__init__.py +0 -0
- {sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black/greeks → sdevpy-1.0.9/sdevpy/volatility/impliedvol}/__init__.py +0 -0
- {sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black_scholes/greeks → sdevpy-1.0.9/sdevpy/volatility/impliedvol/models}/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/fbsabr.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/gsvi.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/mcheston.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/mcsabr.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/mczabr.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/sabr.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/optionsurface.py +0 -0
- {sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black_scholes_merton/greeks → sdevpy-1.0.9/sdevpy/volatility/localvol}/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/localvol/localvol_factory.py +0 -0
- {sdevpy-1.0.8/sdevpy/timeseries → sdevpy-1.0.9/sdevpy/volatility/mlsurfacegen}/__init__.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/mlsurfacegen/fbsabrgenerator.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/mlsurfacegen/mchestongenerator.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/mlsurfacegen/mcsabrgenerator.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/mlsurfacegen/mczabrgenerator.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/mlsurfacegen/sabrgenerator.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/mlsurfacegen/smilegenerator.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/mlsurfacegen/stovolfactory.py +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy.egg-info/dependency_links.txt +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy.egg-info/requires.txt +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy.egg-info/top_level.txt +0 -0
- {sdevpy-1.0.8 → sdevpy-1.0.9}/setup.cfg +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sdevpy"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.9"
|
|
8
8
|
license-files = []
|
|
9
9
|
authors = [{ name="Sebastien Gurrieri", email="sebgur@gmail.com" }]
|
|
10
10
|
description = "Python package for Finance"
|
|
@@ -38,6 +38,17 @@ target-version = "py313"
|
|
|
38
38
|
select = ["E", "F", "N", "W", "UP", "B"]
|
|
39
39
|
ignore = ["E401", "I001"]
|
|
40
40
|
|
|
41
|
+
[tool.coverage.run]
|
|
42
|
+
omit = [
|
|
43
|
+
"sdevpy/**/__init__.py",
|
|
44
|
+
"sdevpy/projects/raschka/*"
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.coverage.report]
|
|
48
|
+
exclude_lines = [
|
|
49
|
+
"if __name__ == '__main__':",
|
|
50
|
+
]
|
|
51
|
+
|
|
41
52
|
[project.urls]
|
|
42
53
|
"Git page" = "https://github.com/sebgur/SDev.Python"
|
|
43
54
|
"SDev Finance" = "http://sdev-finance.com/"
|
|
@@ -6,7 +6,7 @@ from scipy.optimize import minimize_scalar
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def price(expiry: npt.ArrayLike, strike: npt.ArrayLike, is_call: npt.ArrayLike, fwd: npt.ArrayLike,
|
|
9
|
-
vol: npt.ArrayLike) -> npt.
|
|
9
|
+
vol: npt.ArrayLike) -> npt.NDArray[np.float64]:
|
|
10
10
|
""" Option price under the Bachelier model """
|
|
11
11
|
stdev = vol * expiry**0.5
|
|
12
12
|
d = (fwd - strike) / stdev
|
|
@@ -16,22 +16,11 @@ def price(expiry: npt.ArrayLike, strike: npt.ArrayLike, is_call: npt.ArrayLike,
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def price_straddles(expiry: npt.ArrayLike, strike: npt.ArrayLike, fwd: npt.ArrayLike,
|
|
19
|
-
vol: npt.ArrayLike) -> npt.
|
|
20
|
-
""" Straddle price under the Bachelier model
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
prices = []
|
|
25
|
-
for i, exp_row in enumerate(expiries_):
|
|
26
|
-
k_prices = []
|
|
27
|
-
for j, k in enumerate(strike[i]):
|
|
28
|
-
iv = vol[i, j]
|
|
29
|
-
call_price = price(exp_row, k, True, fwd, iv)
|
|
30
|
-
put_price = price(exp_row, k, False, fwd, iv)
|
|
31
|
-
k_prices.append(call_price[0] + put_price[0])
|
|
32
|
-
prices.append(k_prices)
|
|
33
|
-
|
|
34
|
-
return np.asarray(prices)
|
|
19
|
+
vol: npt.ArrayLike) -> npt.NDArray[np.float64]:
|
|
20
|
+
""" Straddle price under the Bachelier model """
|
|
21
|
+
call = price(expiry[:, None], strike, True, fwd, vol)
|
|
22
|
+
put = price(expiry[:, None], strike, False, fwd, vol)
|
|
23
|
+
return call + put
|
|
35
24
|
|
|
36
25
|
|
|
37
26
|
def implied_vol_jaeckel(expiry: float, strike: float, is_call: bool, fwd: float, fwd_price: float) -> float:
|
|
@@ -81,6 +70,8 @@ def implied_vol(expiry: npt.ArrayLike, strike: npt.ArrayLike, is_call: npt.Array
|
|
|
81
70
|
strike = np.asarray(strike, dtype=float)
|
|
82
71
|
fwd_price = np.asarray(fwd_price, dtype=float)
|
|
83
72
|
is_call = np.asarray(is_call, dtype=bool)
|
|
73
|
+
# expiry = float(expiry)
|
|
74
|
+
# fwd = float(fwd)
|
|
84
75
|
|
|
85
76
|
#### ATM branch ####
|
|
86
77
|
m = fwd - strike
|
|
@@ -3,7 +3,6 @@ import numpy as np
|
|
|
3
3
|
import numpy.typing as npt
|
|
4
4
|
from scipy.stats import norm
|
|
5
5
|
from scipy.optimize import minimize_scalar
|
|
6
|
-
from sdevpy.thirdparty.py_vollib.black import implied_volatility as jaeckel
|
|
7
6
|
from sdevpy.utilities.tools import isiterable
|
|
8
7
|
|
|
9
8
|
|
|
@@ -34,7 +33,7 @@ def implied_vol(expiry: float, strike: float, is_call: bool, fwd: float, fwd_pri
|
|
|
34
33
|
|
|
35
34
|
|
|
36
35
|
def implied_vols(expiry: float, strike: npt.ArrayLike, is_call: bool, fwd: float,
|
|
37
|
-
fwd_price: npt.ArrayLike) -> npt.
|
|
36
|
+
fwd_price: npt.ArrayLike) -> npt.NDArray[np.float64]:
|
|
38
37
|
""" Black implied volatility for vector of strikes/prices """
|
|
39
38
|
if isiterable(strike) and isiterable(fwd_price):
|
|
40
39
|
ivs = [implied_vol(expiry, k, is_call, fwd, p) for k, p in zip(strike, fwd_price, strict=True)]
|
|
@@ -46,7 +45,7 @@ def implied_vols(expiry: float, strike: npt.ArrayLike, is_call: bool, fwd: float
|
|
|
46
45
|
|
|
47
46
|
|
|
48
47
|
def implied_vol_newton(expiry: float, strike: npt.ArrayLike, is_call: bool, fwd: float,
|
|
49
|
-
fwd_price: npt.ArrayLike, tol: float=1e-8, max_iter: int=50) -> npt.
|
|
48
|
+
fwd_price: npt.ArrayLike, tol: float=1e-8, max_iter: int=50) -> npt.NDArray[np.float64]:
|
|
50
49
|
""" Using vectorized Newton-Raphson, with faster convergence than Brent.
|
|
51
50
|
However, this method can struggle for very small vegas, so we may want to switch
|
|
52
51
|
to another method (maybe Brent above) below a certain vega threshold.
|
|
@@ -66,22 +65,12 @@ def implied_vol_newton(expiry: float, strike: npt.ArrayLike, is_call: bool, fwd:
|
|
|
66
65
|
if np.all(np.abs(diff) < tol):
|
|
67
66
|
break
|
|
68
67
|
|
|
69
|
-
# if len(strike) == 1 and len(fwd_price) == 1 and len(vol) == 1: # Return a scalar if inputs are scalar
|
|
70
|
-
# return vol[0]
|
|
71
|
-
# else:
|
|
72
|
-
# return vol
|
|
73
|
-
return (vol.item() if vol.ndim == 0 or vol.size ==1 else vol)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def implied_vol_jaeckel(expiry: float, strike: float, is_call: bool, fwd: float, fwd_price: float) -> float:
|
|
77
|
-
""" Black-Scholes implied volatility using P. Jaeckel's 'Let's be rational' method,
|
|
78
|
-
from package py_vollib. Install with pip install py_vollib or at
|
|
79
|
-
https://pypi.org/project/py_vollib/. Unfortunately we found it has instabilities
|
|
80
|
-
near ATM. """
|
|
81
|
-
flag = 'c' if is_call else 'p'
|
|
82
|
-
p = fwd_price
|
|
83
|
-
iv = jaeckel.implied_volatility_of_undiscounted_option_price(p, fwd, strike, expiry, flag)
|
|
84
|
-
return iv
|
|
68
|
+
# # if len(strike) == 1 and len(fwd_price) == 1 and len(vol) == 1: # Return a scalar if inputs are scalar
|
|
69
|
+
# # return vol[0]
|
|
70
|
+
# # else:
|
|
71
|
+
# # return vol
|
|
72
|
+
# return (vol.item() if vol.ndim == 0 or vol.size ==1 else vol)
|
|
73
|
+
return vol
|
|
85
74
|
|
|
86
75
|
|
|
87
76
|
if __name__ == "__main__":
|
|
@@ -98,8 +87,8 @@ if __name__ == "__main__":
|
|
|
98
87
|
k_space = np.linspace(20, 2180, NUM_POINTS)
|
|
99
88
|
prices = price(EXPIRY, k_space, IS_CALL, f_space, VOL)
|
|
100
89
|
# print(prices)
|
|
101
|
-
|
|
90
|
+
iv_results = []
|
|
102
91
|
for i, k in enumerate(k_space):
|
|
103
|
-
|
|
92
|
+
iv_results.append(implied_vol(EXPIRY, k, IS_CALL, f_space[i], prices[i]))
|
|
104
93
|
|
|
105
|
-
# print(
|
|
94
|
+
# print(iv_results)
|
|
@@ -39,8 +39,9 @@ def create_bounds(lw_bounds: list[float], up_bounds: list[float]):
|
|
|
39
39
|
|
|
40
40
|
class Optimizer(ABC):
|
|
41
41
|
@abstractmethod
|
|
42
|
-
def minimize(self, f, x0, args, bounds):
|
|
42
|
+
def minimize(self, f, x0, args=(), bounds=None):
|
|
43
43
|
""" Minimization """
|
|
44
|
+
pass
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
class SciPyOptimizer(Optimizer):
|
|
@@ -57,6 +57,20 @@ def test_black_roundtrip_newton():
|
|
|
57
57
|
|
|
58
58
|
############ Bachelier ############################################################################
|
|
59
59
|
|
|
60
|
+
def test_bachelier_straddle_function():
|
|
61
|
+
""" Straddle prices through straddle function """
|
|
62
|
+
expiry = np.array([0.5, 1.0])
|
|
63
|
+
strike = np.array([[0.03, 0.04, 0.05],
|
|
64
|
+
[0.035, 0.045, 0.055]])
|
|
65
|
+
fwd = 0.04
|
|
66
|
+
vol = np.full((2, 3), 0.05)
|
|
67
|
+
|
|
68
|
+
test = bachelier.price_straddles(expiry, strike, fwd, vol)
|
|
69
|
+
ref = np.asarray([[0.0293304, 0.02820948, 0.0293304], [0.04009353, 0.04009353, 0.04167612]])
|
|
70
|
+
# print(test)
|
|
71
|
+
assert np.allclose(test, ref, 1e-10)
|
|
72
|
+
|
|
73
|
+
|
|
60
74
|
def test_bachelier_price_straddle():
|
|
61
75
|
""" Straddle = Call + Put """
|
|
62
76
|
expiry, fwd, strike, vol = 1.0, 0.04, 0.045, 0.05
|
|
@@ -129,4 +143,4 @@ def test_bachelier_roundtrip_brent():
|
|
|
129
143
|
|
|
130
144
|
|
|
131
145
|
if __name__ == "__main__":
|
|
132
|
-
|
|
146
|
+
test_bachelier_straddle_function()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from statsmodels.tsa.vector_ar.vecm import coint_johansen
|
|
4
|
+
from sdevpy.timeseries.cointegration import johansen_test, check_johansen_stats_fast
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Fixed seed — strong cointegration so results are deterministic
|
|
8
|
+
# _RNG = np.random.default_rng(42)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def make_cointegrated(n=1000, a=2.0, noise_scale=0.005, seed=42):
|
|
12
|
+
"""Two cointegrated series: y2 = a * y1 + small_noise.
|
|
13
|
+
Cointegrating vector (normalised): [1, -1/a].
|
|
14
|
+
"""
|
|
15
|
+
rng = np.random.default_rng(seed)
|
|
16
|
+
# if rng is None:
|
|
17
|
+
# rng = _RNG
|
|
18
|
+
y1 = np.cumsum(rng.normal(0, 1, n))
|
|
19
|
+
y2 = a * y1 + rng.normal(0, noise_scale, n)
|
|
20
|
+
dates = pd.date_range("2000-01-01", periods=n, freq="B")
|
|
21
|
+
return pd.DataFrame({"y1": y1, "y2": y2}, index=dates)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_cointegration_johansen_test_weights_shape():
|
|
25
|
+
"""One weight per asset (column)."""
|
|
26
|
+
df = make_cointegrated()
|
|
27
|
+
result = johansen_test(df)
|
|
28
|
+
assert len(result["weights"]) == df.shape[1]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_cointegration_johansen_test_first_weight_is_one():
|
|
32
|
+
"""norm_1st_eigvec() divides by evec[0][0], so weights[0] must always be 1."""
|
|
33
|
+
result = johansen_test(make_cointegrated())
|
|
34
|
+
assert np.isclose(result["weights"][0], 1.0)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_cointegration_johansen_test_weight_ratio_close_to_minus_1_over_a():
|
|
38
|
+
"""For y2 = a*y1 + noise the ratio w[1]/w[0] must be ≈ -1/a."""
|
|
39
|
+
a = 2.0
|
|
40
|
+
# Extra-strong cointegration and long series for a tight numerical check
|
|
41
|
+
# rng = np.random.default_rng(0)
|
|
42
|
+
df = make_cointegrated(n=2000, a=a, noise_scale=0.001)#, rng=rng)
|
|
43
|
+
w = johansen_test(df)["weights"]
|
|
44
|
+
assert abs(w[1] / w[0] - (-1.0 / a)) < 0.05
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_cointegration_johansen_test_basket_is_stationary():
|
|
48
|
+
""" The Johansen basket w·y must drift far less than y1 alone """
|
|
49
|
+
a = 2.0
|
|
50
|
+
df = make_cointegrated(a=a)
|
|
51
|
+
w = johansen_test(df)["weights"]
|
|
52
|
+
|
|
53
|
+
basket = df["y1"].values * w[0] + df["y2"].values * w[1]
|
|
54
|
+
basket_range = basket.max() - basket.min()
|
|
55
|
+
y1_range = df["y1"].max() - df["y1"].min()
|
|
56
|
+
assert basket_range < 0.1 * y1_range
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_cointegration_johansen_test_detects_cointegration_at_5pct():
|
|
60
|
+
"""A clearly cointegrated pair must pass both trace and eigen tests at 5%."""
|
|
61
|
+
result = johansen_test(make_cointegrated(n=2000, noise_scale=0.001))
|
|
62
|
+
assert result["trace (5%)"]
|
|
63
|
+
assert result["eigen (5%)"]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_cointegration_johansen_test_10pct_at_least_as_liberal_as_5pct():
|
|
67
|
+
"""If a series passes at 5% it must also pass at the looser 10% threshold."""
|
|
68
|
+
result = johansen_test(make_cointegrated())
|
|
69
|
+
if result["trace (5%)"]:
|
|
70
|
+
assert result["trace (10%)"]
|
|
71
|
+
if result["eigen (5%)"]:
|
|
72
|
+
assert result["eigen (10%)"]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_cointegration_johansen_stats_fast_cointegrated_passes():
|
|
76
|
+
"""Clearly cointegrated series must pass all four thresholds."""
|
|
77
|
+
# rng = np.random.default_rng(1)
|
|
78
|
+
df = make_cointegrated(n=2000, noise_scale=0.001)
|
|
79
|
+
res_jo = coint_johansen(df, 0, 1)
|
|
80
|
+
trace_5, trace_10, eigen_5, eigen_10 = check_johansen_stats_fast(res_jo)
|
|
81
|
+
assert trace_10, "trace test at 10% should pass for a clearly cointegrated pair"
|
|
82
|
+
assert eigen_10, "eigen test at 10% should pass for a clearly cointegrated pair"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_cointegration_johansen_stats_fast_consistent_with_johansen_test():
|
|
86
|
+
"""check_johansen_stats_fast results must agree with the flags in johansen_test."""
|
|
87
|
+
df = make_cointegrated()
|
|
88
|
+
high_level = johansen_test(df)
|
|
89
|
+
|
|
90
|
+
res_jo = coint_johansen(df, 0, 1)
|
|
91
|
+
trace_5, trace_10, eigen_5, eigen_10 = check_johansen_stats_fast(res_jo)
|
|
92
|
+
|
|
93
|
+
assert high_level["trace (5%)"] == trace_5
|
|
94
|
+
assert high_level["trace (10%)"] == trace_10
|
|
95
|
+
assert high_level["eigen (5%)"] == eigen_5
|
|
96
|
+
assert high_level["eigen (10%)"] == eigen_10
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
+
from scipy.integrate import quad
|
|
2
3
|
from sdevpy.utilities.tools import isequal
|
|
3
4
|
from sdevpy.volatility.impliedvol.models import svi, biexp, cubicvol, vsvi, gsvi
|
|
4
5
|
from sdevpy.volatility.impliedvol.impliedvol_calib import TsIvObjectiveBuilder
|
|
@@ -8,17 +9,44 @@ from sdevpy.volatility.impliedvol.models.logmix import LogMix
|
|
|
8
9
|
from sdevpy.volatility.impliedvol.models import sabr
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
# n_mix=1, flat vol term structure: beta=1, a=b=0.2, c=0, d=1 → stdev(t=1)=0.2
|
|
13
|
+
_LOGMIX_PARAMS_1 = [1.0, 0.2, 0.2, 0.0, 1.0]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def make_logmix1():
|
|
17
|
+
""" Quick LogMix maker """
|
|
18
|
+
m = LogMix(n_mix=1)
|
|
19
|
+
m.update_params(_LOGMIX_PARAMS_1)
|
|
20
|
+
return m
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_logmix_pdf():
|
|
24
|
+
model = make_logmix1()
|
|
25
|
+
expiry = 1.0
|
|
14
26
|
fwd = 0.04
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
ref = np.asarray([0.54225604, 0.25208659, 0.22711695])
|
|
27
|
+
strikes = np.asarray([0.03, 0.04, 0.05])
|
|
28
|
+
test = model.pdf(expiry, strikes, fwd)
|
|
29
|
+
ref = np.asarray([27.15024656, 49.61906844, 19.05342396])
|
|
19
30
|
assert np.allclose(test, ref, 1e-10)
|
|
20
31
|
|
|
21
32
|
|
|
33
|
+
def test_logmix_pdf_integrates_to_one():
|
|
34
|
+
"""Integral of pdf over (0, ∞) must be ≈ 1."""
|
|
35
|
+
model = make_logmix1()
|
|
36
|
+
integral, _ = quad(lambda k: model.pdf(1.0, k, 1.0), 0.2, 6.0)
|
|
37
|
+
assert abs(integral - 1.0) < 1e-8
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_logmix_cdf_consistent_with_pdf():
|
|
41
|
+
"""CDF(b) - CDF(a) must equal ∫_a^b pdf(k) dk"""
|
|
42
|
+
model = make_logmix1()
|
|
43
|
+
t, fwd, a, b = 1.0, 1.0, 0.8, 1.2
|
|
44
|
+
|
|
45
|
+
cdf_diff = model.cdf(t, b, fwd) - model.cdf(t, a, fwd)
|
|
46
|
+
integral, _ = quad(lambda k: model.pdf(t, k, fwd), a, b)
|
|
47
|
+
assert abs(cdf_diff - integral) < 1e-5
|
|
48
|
+
|
|
49
|
+
|
|
22
50
|
def test_logmix_objective():
|
|
23
51
|
surface = LogMix(2)
|
|
24
52
|
t = np.asarray([0.5, 1.5, 2.5])
|
|
@@ -161,8 +189,21 @@ def test_gsvi_formula():
|
|
|
161
189
|
assert np.allclose(test, ref, 1e-10)
|
|
162
190
|
|
|
163
191
|
|
|
192
|
+
def test_sabr():
|
|
193
|
+
# Test near ATM
|
|
194
|
+
expiry = 0.5
|
|
195
|
+
fwd = 0.04
|
|
196
|
+
params = {'LnVol': 0.25, 'Beta': 0.4, 'Nu': 0.50, 'Rho': -0.25}
|
|
197
|
+
strikes = np.asarray([0.01, 0.04, 0.06])
|
|
198
|
+
test = sabr.sabr_from_dict(expiry, strikes, fwd, params)
|
|
199
|
+
ref = np.asarray([0.54225604, 0.25208659, 0.22711695])
|
|
200
|
+
assert np.allclose(test, ref, 1e-10)
|
|
201
|
+
|
|
202
|
+
|
|
164
203
|
if __name__ == "__main__":
|
|
165
|
-
|
|
204
|
+
test_logmix_pdf_integrates_to_one()
|
|
205
|
+
# test_logmix_pdf()
|
|
206
|
+
# test_sabr()
|
|
166
207
|
# test_tssvi1_objective()
|
|
167
208
|
# test_tssvi1()
|
|
168
209
|
# test_svi_formula()
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import numpy.typing as npt
|
|
3
|
+
from sdevpy.volatility.localvol.localvol import InterpolatedParamLocalVol, MatrixLocalVol
|
|
4
|
+
from sdevpy.volatility.impliedvol.models.svi import SviSection
|
|
5
|
+
from sdevpy.volatility.localvol.dupire import dupire_formula
|
|
6
|
+
from sdevpy.volatility.impliedvol.models.tssvi1 import TsSvi1
|
|
7
|
+
from sdevpy.volatility.impliedvol.models.tssvi2 import TsSvi2
|
|
8
|
+
from sdevpy.volatility.impliedvol.models.logmix import LogMix
|
|
9
|
+
from sdevpy.volatility.localvol.localvol_calib import LvObjectiveBuilder
|
|
10
|
+
from sdevpy.pde import forwardpde as fpde
|
|
11
|
+
from sdevpy.analytics import black
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
############### TEST HELPERS ######################################################################
|
|
15
|
+
VALID_PARAMS = np.array([0.04, 0.1, 0.0, 0.0, 0.2]) # a, b, rho, m, sigma
|
|
16
|
+
T_GRID = np.array([0.25, 0.5, 1.0, 2.0])
|
|
17
|
+
LOGM_GRID = np.array([-0.25, -0.2, 0.0, 0.2, 0.25])
|
|
18
|
+
FLAT_VOL = 0.20
|
|
19
|
+
|
|
20
|
+
# v0, vinf, b_, tau, alpha, beta, r, x0star, lambda0, gamma, delta
|
|
21
|
+
FLAT_TSSVI1_PARAMS = [FLAT_VOL, FLAT_VOL, 0.0, 1.0, 0.0, 0.5, 0.0, 0.0, 0.1, 1.0, 1.0]
|
|
22
|
+
|
|
23
|
+
_T_GRID = np.array([0.5, 1.0])
|
|
24
|
+
FWD = 100.0
|
|
25
|
+
FWDS = [FWD, FWD]
|
|
26
|
+
STRIKES = [np.array([90.0, 100.0, 110.0]),
|
|
27
|
+
np.array([90.0, 100.0, 110.0])]
|
|
28
|
+
# REF_VOL = 0.20
|
|
29
|
+
|
|
30
|
+
# a=0.04, b=0.001>0, rho=0, m=0, sigma=0.1>0 — all svi_check_params constraints satisfied
|
|
31
|
+
VALID_SVI = np.array([0.04, 0.001, 0.0, 0.0, 0.10])
|
|
32
|
+
# b < 0 — fails svi_check_params immediately
|
|
33
|
+
INVALID_SVI = np.array([0.04, -0.10, 0.0, 0.0, 0.10])
|
|
34
|
+
|
|
35
|
+
def make_lv_by_sections(t_grid: list[float]=None, params: npt.ArrayLike=None):
|
|
36
|
+
if t_grid is None:
|
|
37
|
+
t_grid = [0.25, 0.5, 1.0, 2.0]
|
|
38
|
+
if params is None:
|
|
39
|
+
params = VALID_PARAMS
|
|
40
|
+
sections = [SviSection(t) for t in t_grid]
|
|
41
|
+
lv = InterpolatedParamLocalVol(sections)
|
|
42
|
+
for i in range(len(t_grid)):
|
|
43
|
+
lv.update_params(i, params)
|
|
44
|
+
return lv
|
|
45
|
+
|
|
46
|
+
def make_vol_matrix(t_grid=T_GRID, logm_grid=LOGM_GRID):
|
|
47
|
+
""" Bilinear test surface: vol = 0.20 + 0.02*t - 0.05*logm.
|
|
48
|
+
Linear interpolation reproduces this exactly at any interior point. """
|
|
49
|
+
t, lm = np.meshgrid(t_grid, logm_grid, indexing='ij')
|
|
50
|
+
return lv_func_def(t, lm) # 0.20 + 0.02 * t - 0.05 * lm
|
|
51
|
+
|
|
52
|
+
def lv_func_def(t, lm):
|
|
53
|
+
return 0.20 + 0.02 * t - 0.05 * lm
|
|
54
|
+
|
|
55
|
+
def make_mlv(**kwargs):
|
|
56
|
+
""" Make MatrixLocalVol """
|
|
57
|
+
return MatrixLocalVol(T_GRID, LOGM_GRID, make_vol_matrix(), **kwargs)
|
|
58
|
+
|
|
59
|
+
def make_flat_surface():
|
|
60
|
+
s = TsSvi1()
|
|
61
|
+
s.update_params(FLAT_TSSVI1_PARAMS)
|
|
62
|
+
return s
|
|
63
|
+
|
|
64
|
+
def make_tssvi1():
|
|
65
|
+
s = TsSvi1()
|
|
66
|
+
s.update_params(s.initial_point())
|
|
67
|
+
return s
|
|
68
|
+
|
|
69
|
+
def make_tssvi2():
|
|
70
|
+
s = TsSvi2()
|
|
71
|
+
s.update_params(s.initial_point())
|
|
72
|
+
return s
|
|
73
|
+
|
|
74
|
+
def make_logmix2():
|
|
75
|
+
s = LogMix(n_mix=2)
|
|
76
|
+
s.update_params(s.initial_point())
|
|
77
|
+
return s
|
|
78
|
+
|
|
79
|
+
def make_iplv(params=VALID_SVI):
|
|
80
|
+
""" Make InterpolatedParamLocalVol """
|
|
81
|
+
sections = [SviSection(t) for t in _T_GRID]
|
|
82
|
+
lv = InterpolatedParamLocalVol(sections)
|
|
83
|
+
for i in range(len(_T_GRID)):
|
|
84
|
+
lv.update_params(i, params)
|
|
85
|
+
return lv
|
|
86
|
+
|
|
87
|
+
def make_prices():
|
|
88
|
+
return [black.price(exp, STRIKES[i], True, FWD, FLAT_VOL)
|
|
89
|
+
for i, exp in enumerate(_T_GRID)]
|
|
90
|
+
|
|
91
|
+
def make_pde_config():
|
|
92
|
+
return fpde.PdeConfig(n_timesteps=5, n_meshes=30, mesh_vol=FLAT_VOL,
|
|
93
|
+
scheme='rannacher', rescale_x=True, rescale_p=True)
|
|
94
|
+
|
|
95
|
+
def make_builder():
|
|
96
|
+
return LvObjectiveBuilder(make_iplv(), FWDS, STRIKES, make_prices(), make_pde_config())
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
##################### LV calib by sections ########################################################
|
|
100
|
+
def test_lv_bysections_builder_initialize_density_integrates_to_one():
|
|
101
|
+
""" Initial lognormal density on the log-spot grid must integrate to ≈ 1 """
|
|
102
|
+
builder = make_builder()
|
|
103
|
+
old_x, _, old_p = builder.initialize()
|
|
104
|
+
assert abs(np.trapezoid(old_p, old_x) - 1.0) < 0.00001
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_lv_bysections_builder_set_expiry_updates_slice_state():
|
|
108
|
+
builder = make_builder()
|
|
109
|
+
old_x, old_dx, old_p = builder.initialize()
|
|
110
|
+
builder.set_expiry(0, old_x, old_dx, old_p)
|
|
111
|
+
|
|
112
|
+
assert builder.exp_idx == 0
|
|
113
|
+
assert builder.fwd == FWDS[0]
|
|
114
|
+
assert np.allclose(builder.strikes, STRIKES[0])
|
|
115
|
+
assert np.allclose(builder.cf_prices, make_prices()[0])
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_lv_bysections_builder_objective_feasible_params_returns_finite_nonneg():
|
|
119
|
+
""" Feasible params must produce a finite, non-negative RMSE """
|
|
120
|
+
builder = make_builder()
|
|
121
|
+
old_x, old_dx, old_p = builder.initialize()
|
|
122
|
+
builder.set_expiry(0, old_x, old_dx, old_p)
|
|
123
|
+
|
|
124
|
+
result = builder.objective(VALID_SVI)
|
|
125
|
+
|
|
126
|
+
assert np.isfinite(result)
|
|
127
|
+
assert result >= 0.0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_lv_bysections_builder_calculate_vols_has_correct_length_and_positive_values():
|
|
131
|
+
""" calculate_vols() must return one positive vol per strike """
|
|
132
|
+
builder = make_builder()
|
|
133
|
+
old_x, old_dx, old_p = builder.initialize()
|
|
134
|
+
builder.set_expiry(0, old_x, old_dx, old_p)
|
|
135
|
+
builder.objective(VALID_SVI) # Populates pde_prices
|
|
136
|
+
|
|
137
|
+
vols = builder.calculate_vols()
|
|
138
|
+
|
|
139
|
+
assert len(vols) == len(STRIKES[0])
|
|
140
|
+
assert all(v > 0.0 for v in vols)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
##################### Dupire formula ##############################################################
|
|
144
|
+
def test_dupire_impliedvol():
|
|
145
|
+
""" Check Dupire formula by implied vol method """
|
|
146
|
+
x = np.asarray([0.9, 1.0, 1.1])
|
|
147
|
+
test = dupire_formula(make_tssvi1(), ts=0.25, te=1.0, x=x)
|
|
148
|
+
ref = np.asarray([0.36949807, 0.2413907, 0.20621366])
|
|
149
|
+
assert np.allclose(test, ref, 1e-10)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_dupire_pdf():
|
|
153
|
+
""" Check Dupire formula by PDF method """
|
|
154
|
+
x = np.asarray([0.9, 1.0, 1.1])
|
|
155
|
+
test = dupire_formula(make_logmix2(), ts=0.25, te=1.0, x=x)
|
|
156
|
+
ref = np.asarray([0.20000181, 0.20001192, 0.1999994])
|
|
157
|
+
assert np.allclose(test, ref, 1e-10)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_dupire_output_shape():
|
|
161
|
+
""" Output array shape must match input x shape """
|
|
162
|
+
x = np.asarray([0.8, 0.9, 1.0, 1.1, 1.2])
|
|
163
|
+
lv = dupire_formula(make_tssvi2(), ts=0.25, te=1.0, x=x)
|
|
164
|
+
assert lv.shape == x.shape
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_dupire_scalar_input():
|
|
168
|
+
""" Scalar x must return a scalar (0-d array) """
|
|
169
|
+
lv = dupire_formula(make_tssvi2(), ts=0.25, te=1.0, x=1.0)
|
|
170
|
+
assert np.ndim(lv) == 0
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_dupire_ts_near_zero_returns_spot_vol():
|
|
174
|
+
""" When ts < t_threshold the formula falls back to black_volatility(te, x) """
|
|
175
|
+
surface = make_tssvi2()
|
|
176
|
+
x = np.asarray([0.9, 1.0, 1.1])
|
|
177
|
+
lv = dupire_formula(surface, ts=0.0, te=1.0, x=x)
|
|
178
|
+
expected = surface.black_volatility(t=1.0, k=x, f=1.0)
|
|
179
|
+
assert np.allclose(lv, expected)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_dupire_flat_surface_recovers_constant_vol():
|
|
183
|
+
""" On a flat vol surface (no skew, flat term structure) Dupire LV = IV """
|
|
184
|
+
x = np.asarray([0.8, 0.9, 1.0, 1.1, 1.2])
|
|
185
|
+
lv = dupire_formula(make_flat_surface(), ts=0.5, te=1.0, x=x)
|
|
186
|
+
assert np.allclose(lv, FLAT_VOL, atol=1e-6)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
##################### LV by matrix interpolation ##################################################
|
|
190
|
+
def test_lv_bymatrix_pchip_def():
|
|
191
|
+
lv = MatrixLocalVol(T_GRID, LOGM_GRID, make_vol_matrix(), interpolation='pchip')
|
|
192
|
+
assert lv.method == 'pchip'
|
|
193
|
+
lv2 = MatrixLocalVol(T_GRID, LOGM_GRID, make_vol_matrix(), interpolation='cubic')
|
|
194
|
+
assert lv2.method == 'cubic'
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_lv_bymatrix_value_on_grid_nodes():
|
|
198
|
+
""" Values at grid nodes must exactly reproduce the input matrix. """
|
|
199
|
+
lv = make_mlv()
|
|
200
|
+
vol_matrix = make_vol_matrix()
|
|
201
|
+
for i, t in enumerate(T_GRID):
|
|
202
|
+
for j, lm in enumerate(LOGM_GRID):
|
|
203
|
+
assert np.isclose(lv.value(t, lm), vol_matrix[i, j])
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_lv_bymatrix_value_interior_bilinear_exact():
|
|
207
|
+
""" Linear interpolation on a bilinear surface is exact at any interior point. """
|
|
208
|
+
lv = make_mlv()
|
|
209
|
+
t, lm = 0.75, 0.1
|
|
210
|
+
# expected = 0.20 + 0.02 * t - 0.05 * lm
|
|
211
|
+
expected = lv_func_def(t, lm)
|
|
212
|
+
assert np.isclose(lv.value(t, lm), expected, atol=1e-12)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_lv_bymatrix_extrap_below_t():
|
|
216
|
+
""" t below grid must return the same value as t_grid[0]. """
|
|
217
|
+
lv = make_mlv()
|
|
218
|
+
assert np.isclose(lv.value(0.0, 0.0), lv.value(T_GRID[0], 0.0))
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_lv_bymatrix_extrap_above_t():
|
|
222
|
+
""" t above grid must return the same value as t_grid[-1]. """
|
|
223
|
+
lv = make_mlv()
|
|
224
|
+
assert np.isclose(lv.value(100.0, 0.0), lv.value(T_GRID[-1], 0.0))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_lv_bymatrix_extrap_below_logm():
|
|
228
|
+
""" logm below grid must return the same value as logm_grid[0]. """
|
|
229
|
+
lv = make_mlv()
|
|
230
|
+
assert np.isclose(lv.value(1.0, -10.0), lv.value(1.0, LOGM_GRID[0]))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def test_lv_bymatrix_extrap_above_logm():
|
|
234
|
+
""" logm above grid must return the same value as logm_grid[-1]. """
|
|
235
|
+
lv = make_mlv()
|
|
236
|
+
assert np.isclose(lv.value(1.0, 10.0), lv.value(1.0, LOGM_GRID[-1]))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_lv_bymatrix_flat_surface_everywhere():
|
|
240
|
+
""" A flat vol surface must return the same value at all (t, logm), including outside the grid. """
|
|
241
|
+
lv = MatrixLocalVol(T_GRID, LOGM_GRID, np.full((4, 5), 0.25))
|
|
242
|
+
for t in [0.0, 0.5, 1.5, 5.0]:
|
|
243
|
+
for lm in [-1.0, 0.0, 1.0]:
|
|
244
|
+
assert np.isclose(lv.value(t, lm), 0.25)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_lv_bymatrix_section_consistent_with_value():
|
|
248
|
+
""" section(t)(logm) must equal value(t, logm) for any logm. """
|
|
249
|
+
lv = make_mlv()
|
|
250
|
+
t = 0.75
|
|
251
|
+
logm = np.array([-0.15, 0.0, 0.15])
|
|
252
|
+
assert np.allclose(lv.section(t)(logm), lv.value(t, logm))
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
##################### LV by sections ##############################################################
|
|
256
|
+
def test_lv_sections_sorted_by_time():
|
|
257
|
+
""" Sections passed in reverse order should be stored sorted by time """
|
|
258
|
+
sections = [SviSection(t) for t in [2.0, 0.5, 1.0]]
|
|
259
|
+
lv = InterpolatedParamLocalVol(sections)
|
|
260
|
+
times = [s.time for s in lv.sections]
|
|
261
|
+
assert times == sorted(times)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def test_lv_t_grid_matches_sections():
|
|
265
|
+
""" LV's time grid matches sections' """
|
|
266
|
+
sections = [SviSection(t) for t in [3.0, 1.0, 0.5]]
|
|
267
|
+
lv = InterpolatedParamLocalVol(sections)
|
|
268
|
+
assert lv.t_grid == [0.5, 1.0, 3.0]
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def test_lv_by_section_values():
|
|
272
|
+
"""Between pillars, value() must delegate to the upper (right) section."""
|
|
273
|
+
t_grid = [0.5, 1.0, 2.0]
|
|
274
|
+
params_mid = np.array([0.04, 0.1, 0.0, 0.0, 0.2])
|
|
275
|
+
params_high = np.array([0.09, 0.2, 0.0, 0.0, 0.3])
|
|
276
|
+
sections = [SviSection(t) for t in t_grid]
|
|
277
|
+
lv = InterpolatedParamLocalVol(sections)
|
|
278
|
+
lv.update_params(0, params_mid)
|
|
279
|
+
lv.update_params(1, params_high)
|
|
280
|
+
lv.update_params(2, params_mid)
|
|
281
|
+
|
|
282
|
+
logm = [-0.5, 0.0, 0.5]
|
|
283
|
+
test = lv.value(0.75, logm)
|
|
284
|
+
# print(test)
|
|
285
|
+
ref = np.asarray([0.52487337, 0.4472136, 0.52487337,])
|
|
286
|
+
assert np.allclose(test, ref, 1e-10)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def test_lv_sections_return_time():
|
|
290
|
+
lv = make_lv_by_sections(t_grid=[0.5, 1.0])
|
|
291
|
+
assert lv.section(0).time == 0.5
|
|
292
|
+
assert lv.section(1).time == 1.0
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def test_lv_update_params_do_not_mutate():
|
|
296
|
+
""" Mutating the source array after update_params must not change stored params """
|
|
297
|
+
lv = make_lv_by_sections(t_grid=[1.0])
|
|
298
|
+
p = np.array([0.04, 0.1, 0.0, 0.0, 0.2])
|
|
299
|
+
lv.update_params(0, p)
|
|
300
|
+
p[0] = 999.0
|
|
301
|
+
assert lv.params(0)[0] != 999.0
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def test_lv_check_params():
|
|
305
|
+
lv = make_lv_by_sections()
|
|
306
|
+
is_ok, penalty = lv.check_params(0)
|
|
307
|
+
assert is_ok
|
|
308
|
+
assert penalty == 0.0
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_lv_bysections_dump_data_keys():
|
|
312
|
+
data = make_lv_by_sections().dump_data()
|
|
313
|
+
assert set(data.keys()) == {'name', 'valdate', 'snapdate', 'sections'}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
if __name__ == "__main__":
|
|
317
|
+
test_dupire_impliedvol()
|
|
318
|
+
test_dupire_pdf()
|