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.
Files changed (223) hide show
  1. {sdevpy-1.0.8 → sdevpy-1.0.9}/PKG-INFO +1 -1
  2. {sdevpy-1.0.8 → sdevpy-1.0.9}/pyproject.toml +12 -1
  3. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/analytics/bachelier.py +8 -17
  4. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/analytics/black.py +11 -22
  5. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/optimization.py +2 -1
  6. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_analytics.py +15 -1
  7. sdevpy-1.0.9/sdevpy/tests/test_cointegration.py +96 -0
  8. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_impliedvol.py +49 -8
  9. sdevpy-1.0.9/sdevpy/tests/test_localvol.py +318 -0
  10. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/timeseries/backtesting.py +2 -1
  11. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/timeseries/cointegration.py +11 -41
  12. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/timeseries/meanreversion.py +11 -0
  13. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/timeseries/timeseriestools.py +11 -10
  14. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/tools.py +3 -0
  15. sdevpy-1.0.8/sdevpy/volatility/impliedvol/zerosurface.py → sdevpy-1.0.9/sdevpy/volatility/impliedvol/impliedvol.py +93 -80
  16. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/impliedvol_calib.py +7 -5
  17. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/biexp.py +3 -3
  18. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/cubicvol.py +5 -3
  19. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/logmix.py +13 -7
  20. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/svi.py +4 -3
  21. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/tssvi1.py +8 -5
  22. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/tssvi2.py +7 -2
  23. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/vsvi.py +3 -3
  24. sdevpy-1.0.9/sdevpy/volatility/localvol/dupire.py +197 -0
  25. sdevpy-1.0.9/sdevpy/volatility/localvol/localvol.py +192 -0
  26. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/localvol/localvol_calib.py +6 -2
  27. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy.egg-info/PKG-INFO +1 -1
  28. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy.egg-info/SOURCES.txt +2 -54
  29. sdevpy-1.0.8/sdevpy/cointegration/back_testing.py +0 -224
  30. sdevpy-1.0.8/sdevpy/cointegration/coint_trading.py +0 -677
  31. sdevpy-1.0.8/sdevpy/cointegration/data_io.py +0 -14
  32. sdevpy-1.0.8/sdevpy/cointegration/mean_reversion.py +0 -268
  33. sdevpy-1.0.8/sdevpy/cointegration/model_settings.py +0 -34
  34. sdevpy-1.0.8/sdevpy/cointegration/utils.py +0 -132
  35. sdevpy-1.0.8/sdevpy/tests/test_localvol.py +0 -0
  36. sdevpy-1.0.8/sdevpy/thirdparty/py_lets_be_rational/__init__.py +0 -49
  37. sdevpy-1.0.8/sdevpy/thirdparty/py_lets_be_rational/constants.py +0 -65
  38. sdevpy-1.0.8/sdevpy/thirdparty/py_lets_be_rational/erf_cody.py +0 -448
  39. sdevpy-1.0.8/sdevpy/thirdparty/py_lets_be_rational/exceptions.py +0 -68
  40. sdevpy-1.0.8/sdevpy/thirdparty/py_lets_be_rational/lets_be_rational.py +0 -801
  41. sdevpy-1.0.8/sdevpy/thirdparty/py_lets_be_rational/normaldistribution.py +0 -194
  42. sdevpy-1.0.8/sdevpy/thirdparty/py_lets_be_rational/numba_helper.py +0 -13
  43. sdevpy-1.0.8/sdevpy/thirdparty/py_lets_be_rational/rationalcubic.py +0 -272
  44. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/__init__.py +0 -32
  45. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black/__init__.py +0 -180
  46. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black/greeks/analytical.py +0 -273
  47. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black/greeks/numerical.py +0 -251
  48. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black/implied_volatility.py +0 -292
  49. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black_scholes/__init__.py +0 -84
  50. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black_scholes/greeks/analytical.py +0 -278
  51. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black_scholes/greeks/numerical.py +0 -318
  52. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black_scholes/implied_volatility.py +0 -102
  53. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black_scholes_merton/__init__.py +0 -90
  54. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black_scholes_merton/greeks/analytical.py +0 -309
  55. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black_scholes_merton/greeks/numerical.py +0 -266
  56. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black_scholes_merton/implied_volatility.py +0 -116
  57. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/helpers/__init__.py +0 -114
  58. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/helpers/constants.py +0 -52
  59. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/helpers/distributions.py +0 -226
  60. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/helpers/doctest_helper.py +0 -62
  61. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/helpers/exceptions.py +0 -59
  62. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/helpers/numerical_greeks.py +0 -217
  63. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/__init__.py +0 -32
  64. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black/__init__.py +0 -236
  65. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black/greeks/analytical.py +0 -277
  66. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black/greeks/numerical.py +0 -222
  67. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black/implied_volatility.py +0 -117
  68. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black_scholes/__init__.py +0 -157
  69. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black_scholes/greeks/analytical.py +0 -279
  70. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black_scholes/greeks/numerical.py +0 -290
  71. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black_scholes/implied_volatility.py +0 -110
  72. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black_scholes_merton/__init__.py +0 -231
  73. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black_scholes_merton/greeks/analytical.py +0 -311
  74. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black_scholes_merton/greeks/numerical.py +0 -223
  75. sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black_scholes_merton/implied_volatility.py +0 -104
  76. sdevpy-1.0.8/sdevpy/tree/__init__.py +0 -0
  77. sdevpy-1.0.8/sdevpy/utilities/__init__.py +0 -0
  78. sdevpy-1.0.8/sdevpy/volatility/__init__.py +0 -0
  79. sdevpy-1.0.8/sdevpy/volatility/impliedvol/__init__.py +0 -0
  80. sdevpy-1.0.8/sdevpy/volatility/impliedvol/impliedvol.py +0 -45
  81. sdevpy-1.0.8/sdevpy/volatility/impliedvol/models/__init__.py +0 -0
  82. sdevpy-1.0.8/sdevpy/volatility/localvol/__init__.py +0 -0
  83. sdevpy-1.0.8/sdevpy/volatility/localvol/localvol.py +0 -92
  84. sdevpy-1.0.8/sdevpy/volatility/mlsurfacegen/__init__.py +0 -0
  85. {sdevpy-1.0.8 → sdevpy-1.0.9}/README.md +0 -0
  86. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/__init__.py +0 -0
  87. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/analytics/__init__.py +0 -0
  88. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/analytics/americantree.py +0 -0
  89. {sdevpy-1.0.8/sdevpy/cointegration → sdevpy-1.0.9/sdevpy/machinelearning}/__init__.py +0 -0
  90. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/datasets.py +0 -0
  91. {sdevpy-1.0.8/sdevpy/machinelearning → sdevpy-1.0.9/sdevpy/machinelearning/keras}/__init__.py +0 -0
  92. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/keras/callbacks.py +0 -0
  93. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/keras/learningmodel.py +0 -0
  94. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/keras/learningschedules.py +0 -0
  95. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/keras/topology.py +0 -0
  96. {sdevpy-1.0.8/sdevpy/machinelearning/keras → sdevpy-1.0.9/sdevpy/machinelearning/llms}/__init__.py +0 -0
  97. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/llms/attention.py +0 -0
  98. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/llms/chat.py +0 -0
  99. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/llms/datasets.py +0 -0
  100. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/llms/gpt.py +0 -0
  101. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/llms/instructions.py +0 -0
  102. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/llms/modelconverter.py +0 -0
  103. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/llms/textgen.py +0 -0
  104. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/llms/tokenizers.py +0 -0
  105. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/machinelearning/llms/training.py +0 -0
  106. {sdevpy-1.0.8/sdevpy/machinelearning/llms → sdevpy-1.0.9/sdevpy/market}/__init__.py +0 -0
  107. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/market/correlations.py +0 -0
  108. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/market/eqforward.py +0 -0
  109. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/market/eqvolsurface.py +0 -0
  110. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/market/fixings.py +0 -0
  111. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/market/spot.py +0 -0
  112. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/market/yieldcurve.py +0 -0
  113. {sdevpy-1.0.8/sdevpy/market → sdevpy-1.0.9/sdevpy/maths}/__init__.py +0 -0
  114. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/constants.py +0 -0
  115. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/integration.py +0 -0
  116. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/interpolation.py +0 -0
  117. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/metrics.py +0 -0
  118. {sdevpy-1.0.8/sdevpy/maths → sdevpy-1.0.9/sdevpy/maths/rand}/__init__.py +0 -0
  119. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/rand/correlations.py +0 -0
  120. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/rand/pathconstruction.py +0 -0
  121. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/rand/rng.py +0 -0
  122. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/regression.py +0 -0
  123. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/sets.py +0 -0
  124. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/specialfunctions.py +0 -0
  125. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/maths/tridiag.py +0 -0
  126. {sdevpy-1.0.8/sdevpy/maths/rand → sdevpy-1.0.9/sdevpy/models}/__init__.py +0 -0
  127. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/models/assetmodels.py +0 -0
  128. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/models/multiasset_heston.py +0 -0
  129. {sdevpy-1.0.8/sdevpy/models → sdevpy-1.0.9/sdevpy/montecarlo}/__init__.py +0 -0
  130. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/montecarlo/mcpricer.py +0 -0
  131. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/montecarlo/mcrun.py +0 -0
  132. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/montecarlo/pathgenerator.py +0 -0
  133. {sdevpy-1.0.8/sdevpy/montecarlo → sdevpy-1.0.9/sdevpy/montecarlo/payoffs}/__init__.py +0 -0
  134. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/montecarlo/payoffs/basic.py +0 -0
  135. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/montecarlo/payoffs/cashflows.py +0 -0
  136. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/montecarlo/payoffs/exotics.py +0 -0
  137. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/montecarlo/payoffs/vanillas.py +0 -0
  138. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/montecarlo/smoothers.py +0 -0
  139. {sdevpy-1.0.8/sdevpy/montecarlo/payoffs → sdevpy-1.0.9/sdevpy/pde}/__init__.py +0 -0
  140. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/pde/forwardpde.py +0 -0
  141. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/pde/pdeschemes.py +0 -0
  142. {sdevpy-1.0.8/sdevpy/pde → sdevpy-1.0.9/sdevpy/projects}/__init__.py +0 -0
  143. {sdevpy-1.0.8/sdevpy/projects → sdevpy-1.0.9/sdevpy/projects/aad}/__init__.py +0 -0
  144. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/aad/aad_mc.py +0 -0
  145. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/aad/aad_mc_nd.py +0 -0
  146. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/chat_gpt2.py +0 -0
  147. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/datafiles.py +0 -0
  148. {sdevpy-1.0.8/sdevpy/projects/aad → sdevpy-1.0.9/sdevpy/projects/raschka}/__init__.py +0 -0
  149. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/raschka/ch2_working_with_text.py +0 -0
  150. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/raschka/ch3_coding_attention.py +0 -0
  151. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/raschka/ch4_gpt_model.py +0 -0
  152. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/raschka/ch5_loadgpt2.py +0 -0
  153. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/raschka/ch5_pretraining.py +0 -0
  154. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/raschka/ch7_instruction_finetuning.py +0 -0
  155. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/raschka/raschka_datasetloader.py +0 -0
  156. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/raschka/raschka_dnn.py +0 -0
  157. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/raschka/raschka_gpt_download.py +0 -0
  158. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/set_limits.py +0 -0
  159. {sdevpy-1.0.8/sdevpy/projects/raschka → sdevpy-1.0.9/sdevpy/projects/stovol}/__init__.py +0 -0
  160. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/stovol/stovolgen.py +0 -0
  161. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/stovol/stovolplot.py +0 -0
  162. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/stovol/stovoltrain.py +0 -0
  163. {sdevpy-1.0.8/sdevpy/projects/stovol → sdevpy-1.0.9/sdevpy/projects/stovolinverse}/__init__.py +0 -0
  164. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/stovolinverse/stovolinvgen.py +0 -0
  165. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/stovolinverse/stovolinvtrain.py +0 -0
  166. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/projects/update_db.py +0 -0
  167. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/settings.py +0 -0
  168. {sdevpy-1.0.8/sdevpy/projects/stovolinverse → sdevpy-1.0.9/sdevpy/tensorflow}/__init__.py +0 -0
  169. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tensorflow/tf_black.py +0 -0
  170. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tensorflow/tf_metrics.py +0 -0
  171. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/__init__.py +0 -0
  172. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test.py +0 -0
  173. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_algos.py +0 -0
  174. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_dates.py +0 -0
  175. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_interpolation.py +0 -0
  176. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_marketdata.py +0 -0
  177. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_mc.py +0 -0
  178. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_pde.py +0 -0
  179. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_timegrids.py +0 -0
  180. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_utils.py +0 -0
  181. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tests/test_yieldcurves.py +0 -0
  182. {sdevpy-1.0.8/sdevpy/tensorflow → sdevpy-1.0.9/sdevpy/thirdparty}/__init__.py +0 -0
  183. {sdevpy-1.0.8/sdevpy/thirdparty → sdevpy-1.0.9/sdevpy/timeseries}/__init__.py +0 -0
  184. {sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black/greeks → sdevpy-1.0.9/sdevpy/tree}/__init__.py +0 -0
  185. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/tree/trees.py +0 -0
  186. {sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black_scholes/greeks → sdevpy-1.0.9/sdevpy/utilities}/__init__.py +0 -0
  187. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/algos.py +0 -0
  188. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/book.py +0 -0
  189. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/clipboard.py +0 -0
  190. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/constants.py +0 -0
  191. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/dates.py +0 -0
  192. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/filemanager.py +0 -0
  193. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/jsonmanager.py +0 -0
  194. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/network.py +0 -0
  195. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/pydotnet.py +0 -0
  196. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/scalendar.py +0 -0
  197. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/speriods.py +0 -0
  198. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/timegrids.py +0 -0
  199. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/utilities/timer.py +0 -0
  200. {sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/black_scholes_merton/greeks → sdevpy-1.0.9/sdevpy/volatility}/__init__.py +0 -0
  201. {sdevpy-1.0.8/sdevpy/thirdparty/py_vollib/ref_python/black/greeks → sdevpy-1.0.9/sdevpy/volatility/impliedvol}/__init__.py +0 -0
  202. {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
  203. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/fbsabr.py +0 -0
  204. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/gsvi.py +0 -0
  205. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/mcheston.py +0 -0
  206. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/mcsabr.py +0 -0
  207. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/mczabr.py +0 -0
  208. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/models/sabr.py +0 -0
  209. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/impliedvol/optionsurface.py +0 -0
  210. {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
  211. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/localvol/localvol_factory.py +0 -0
  212. {sdevpy-1.0.8/sdevpy/timeseries → sdevpy-1.0.9/sdevpy/volatility/mlsurfacegen}/__init__.py +0 -0
  213. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/mlsurfacegen/fbsabrgenerator.py +0 -0
  214. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/mlsurfacegen/mchestongenerator.py +0 -0
  215. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/mlsurfacegen/mcsabrgenerator.py +0 -0
  216. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/mlsurfacegen/mczabrgenerator.py +0 -0
  217. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/mlsurfacegen/sabrgenerator.py +0 -0
  218. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/mlsurfacegen/smilegenerator.py +0 -0
  219. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy/volatility/mlsurfacegen/stovolfactory.py +0 -0
  220. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy.egg-info/dependency_links.txt +0 -0
  221. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy.egg-info/requires.txt +0 -0
  222. {sdevpy-1.0.8 → sdevpy-1.0.9}/sdevpy.egg-info/top_level.txt +0 -0
  223. {sdevpy-1.0.8 → sdevpy-1.0.9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdevpy
3
- Version: 1.0.8
3
+ Version: 1.0.9
4
4
  Summary: Python package for Finance
5
5
  Author-email: Sebastien Gurrieri <sebgur@gmail.com>
6
6
  Project-URL: Git page, https://github.com/sebgur/SDev.Python
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sdevpy"
7
- version = "1.0.8"
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.ArrayLike:
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.ArrayLike:
20
- """ Straddle price under the Bachelier model.
21
- Note: we could improve the speed by writing the code in-line instead of
22
- calling the price() function twice """
23
- expiries_ = np.asarray(expiry).reshape(-1, 1)
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.ArrayLike:
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.ArrayLike:
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
- implied_vols = []
90
+ iv_results = []
102
91
  for i, k in enumerate(k_space):
103
- implied_vols.append(implied_vol(EXPIRY, k, IS_CALL, f_space[i], prices[i]))
92
+ iv_results.append(implied_vol(EXPIRY, k, IS_CALL, f_space[i], prices[i]))
104
93
 
105
- # print(implied_vols)
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
- test_bachelier_roundtrip()
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
- def test_sabr():
12
- # Test near ATM
13
- expiry = 0.5
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
- params = {'LnVol': 0.25, 'Beta': 0.4, 'Nu': 0.50, 'Rho': -0.25}
16
- strikes = np.asarray([0.01, 0.04, 0.06])
17
- test = sabr.sabr_from_dict(expiry, strikes, fwd, params)
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
- test_sabr()
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()