optPricing 2.4.0__tar.gz → 2.5.0__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 (193) hide show
  1. optpricing-2.5.0/CHANGELOG.md +50 -0
  2. {optpricing-2.4.0/src/optpricing.egg-info → optpricing-2.5.0}/PKG-INFO +1 -1
  3. optpricing-2.5.0/benchmarks/run_benchmarks.py +164 -0
  4. {optpricing-2.4.0 → optpricing-2.5.0}/pyproject.toml +4 -0
  5. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/calibration/iv_surface.py +2 -2
  6. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/cli/commands/dashboard.py +2 -1
  7. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/cli/commands/price.py +1 -1
  8. optpricing-2.5.0/src/optpricing/config.py +66 -0
  9. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/data/historical_manager.py +1 -0
  10. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/data/market_data_manager.py +2 -2
  11. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/models/base/__init__.py +2 -0
  12. optpricing-2.5.0/src/optpricing/models/base/cf_sampler.py +110 -0
  13. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/models/bates.py +11 -4
  14. optpricing-2.5.0/src/optpricing/models/cev.py +145 -0
  15. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/models/cgmy.py +54 -31
  16. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/models/cir.py +6 -1
  17. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/models/heston.py +17 -7
  18. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/models/hyperbolic.py +51 -19
  19. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/models/kou.py +7 -0
  20. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/models/nig.py +15 -2
  21. optpricing-2.5.0/src/optpricing/models/sabr.py +290 -0
  22. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/models/sabr_jump.py +36 -3
  23. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/models/vg.py +7 -0
  24. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/american_monte_carlo.py +14 -4
  25. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/kernels/american_mc_kernels.py +16 -15
  26. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/kernels/mc_kernels.py +35 -27
  27. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/kernels/path_kernels.py +34 -24
  28. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/monte_carlo.py +22 -14
  29. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/pde.py +68 -23
  30. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/workflows/backtest_workflow.py +1 -0
  31. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/workflows/configs/sabr_config.py +1 -0
  32. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/workflows/configs/sabr_jump_config.py +2 -1
  33. {optpricing-2.4.0 → optpricing-2.5.0/src/optpricing.egg-info}/PKG-INFO +1 -1
  34. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing.egg-info/SOURCES.txt +9 -0
  35. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing.egg-info/scm_file_list.json +9 -0
  36. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing.egg-info/scm_version.json +2 -2
  37. optpricing-2.5.0/tests/correctness/__init__.py +0 -0
  38. optpricing-2.5.0/tests/correctness/_helpers.py +17 -0
  39. optpricing-2.5.0/tests/correctness/conftest.py +128 -0
  40. optpricing-2.5.0/tests/correctness/test_invariants.py +285 -0
  41. optpricing-2.5.0/tests/correctness/test_mc_regressions.py +344 -0
  42. optpricing-2.5.0/tests/correctness/test_reference_values.py +427 -0
  43. {optpricing-2.4.0 → optpricing-2.5.0}/tests/models/test_cgmy.py +35 -21
  44. optpricing-2.5.0/tests/models/test_sabr.py +138 -0
  45. {optpricing-2.4.0 → optpricing-2.5.0}/tests/models/test_sabr_jump.py +17 -5
  46. {optpricing-2.4.0 → optpricing-2.5.0}/tests/techniques/kernels/test_mc_kernels.py +12 -17
  47. {optpricing-2.4.0 → optpricing-2.5.0}/tests/techniques/kernels/test_path_kernels.py +7 -5
  48. optpricing-2.5.0/tests/test_config.py +70 -0
  49. optpricing-2.4.0/CHANGELOG.md +0 -30
  50. optpricing-2.4.0/src/optpricing/config.py +0 -57
  51. optpricing-2.4.0/src/optpricing/models/cev.py +0 -109
  52. optpricing-2.4.0/src/optpricing/models/sabr.py +0 -64
  53. optpricing-2.4.0/tests/models/test_sabr.py +0 -40
  54. {optpricing-2.4.0 → optpricing-2.5.0}/LICENSE +0 -0
  55. {optpricing-2.4.0 → optpricing-2.5.0}/MANIFEST.in +0 -0
  56. {optpricing-2.4.0 → optpricing-2.5.0}/README.md +0 -0
  57. {optpricing-2.4.0 → optpricing-2.5.0}/setup.cfg +0 -0
  58. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/__init__.py +0 -0
  59. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/atoms/__init__.py +0 -0
  60. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/atoms/bond.py +0 -0
  61. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/atoms/option.py +0 -0
  62. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/atoms/rate.py +0 -0
  63. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/atoms/stock.py +0 -0
  64. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/calibration/__init__.py +0 -0
  65. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/calibration/calibrator.py +0 -0
  66. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/calibration/fit_jump_parameters.py +0 -0
  67. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/calibration/fit_market_params.py +0 -0
  68. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/calibration/technique_selector.py +0 -0
  69. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/calibration/vectorized_bsm_iv.py +0 -0
  70. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/calibration/vectorized_integration_iv.py +0 -0
  71. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/calibration/vectorized_pricer.py +0 -0
  72. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/cli/__init__.py +0 -0
  73. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/cli/commands/__init__.py +0 -0
  74. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/cli/commands/backtest.py +0 -0
  75. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/cli/commands/calibrate.py +0 -0
  76. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/cli/commands/data.py +0 -0
  77. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/cli/commands/demo.py +0 -0
  78. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/cli/commands/tools.py +0 -0
  79. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/cli/main.py +0 -0
  80. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/dashboard/Home.py +0 -0
  81. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/dashboard/__init__.py +0 -0
  82. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/dashboard/pages/1_Pricer_and_Greeks.py +0 -0
  83. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/dashboard/pages/2_Calibration_and_IV.py +0 -0
  84. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/dashboard/pages/3_Market_Analytics.py +0 -0
  85. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/dashboard/pages/4_Model_Fitting.py +0 -0
  86. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/dashboard/pages/5_Term_Structure.py +0 -0
  87. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/dashboard/pages/__init__.py +0 -0
  88. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/dashboard/plots.py +0 -0
  89. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/dashboard/service.py +0 -0
  90. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/dashboard/widgets.py +0 -0
  91. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/data/__init__.py +0 -0
  92. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/models/__init__.py +0 -0
  93. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/models/base/base_model.py +0 -0
  94. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/models/base/validators.py +0 -0
  95. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/models/blacks_approx.py +0 -0
  96. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/models/bsm.py +0 -0
  97. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/models/dupire_local.py +0 -0
  98. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/models/merton_jump.py +0 -0
  99. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/models/perpetual_put.py +0 -0
  100. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/models/vasicek.py +0 -0
  101. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/parity/__init__.py +0 -0
  102. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/parity/implied_rate.py +0 -0
  103. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/parity/parity_model.py +0 -0
  104. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/__init__.py +0 -0
  105. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/base/__init__.py +0 -0
  106. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/base/base_technique.py +0 -0
  107. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/base/greek_mixin.py +0 -0
  108. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/base/iv_mixin.py +0 -0
  109. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/base/lattice_technique.py +0 -0
  110. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/base/pricing_result.py +0 -0
  111. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/base/random_utils.py +0 -0
  112. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/closed_form.py +0 -0
  113. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/crr.py +0 -0
  114. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/fft.py +0 -0
  115. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/integration.py +0 -0
  116. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/kernels/__init__.py +0 -0
  117. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/kernels/lattice_kernels.py +0 -0
  118. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/leisen_reimer.py +0 -0
  119. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/techniques/topm.py +0 -0
  120. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/workflows/__init__.py +0 -0
  121. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/workflows/configs/__init__.py +0 -0
  122. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/workflows/configs/bates_config.py +0 -0
  123. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/workflows/configs/bsm_config.py +0 -0
  124. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/workflows/configs/cev_config.py +0 -0
  125. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/workflows/configs/cgmy_config.py +0 -0
  126. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/workflows/configs/heston_config.py +0 -0
  127. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/workflows/configs/hyperbolic_config.py +0 -0
  128. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/workflows/configs/kou_config.py +0 -0
  129. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/workflows/configs/merton_config.py +0 -0
  130. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/workflows/configs/nig_config.py +0 -0
  131. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/workflows/configs/vg_config.py +0 -0
  132. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing/workflows/daily_workflow.py +0 -0
  133. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing.egg-info/dependency_links.txt +0 -0
  134. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing.egg-info/entry_points.txt +0 -0
  135. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing.egg-info/requires.txt +0 -0
  136. {optpricing-2.4.0 → optpricing-2.5.0}/src/optpricing.egg-info/top_level.txt +0 -0
  137. {optpricing-2.4.0 → optpricing-2.5.0}/tests/atoms/test_bond.py +0 -0
  138. {optpricing-2.4.0 → optpricing-2.5.0}/tests/atoms/test_option.py +0 -0
  139. {optpricing-2.4.0 → optpricing-2.5.0}/tests/atoms/test_rate.py +0 -0
  140. {optpricing-2.4.0 → optpricing-2.5.0}/tests/atoms/test_stock.py +0 -0
  141. {optpricing-2.4.0 → optpricing-2.5.0}/tests/calibration/test_calibrator.py +0 -0
  142. {optpricing-2.4.0 → optpricing-2.5.0}/tests/calibration/test_fit_jump_parameters.py +0 -0
  143. {optpricing-2.4.0 → optpricing-2.5.0}/tests/calibration/test_fit_market_params.py +0 -0
  144. {optpricing-2.4.0 → optpricing-2.5.0}/tests/calibration/test_iv_surface.py +0 -0
  145. {optpricing-2.4.0 → optpricing-2.5.0}/tests/calibration/test_technique_selector.py +0 -0
  146. {optpricing-2.4.0 → optpricing-2.5.0}/tests/calibration/test_vectorized_bsm_iv.py +0 -0
  147. {optpricing-2.4.0 → optpricing-2.5.0}/tests/calibration/test_vectorized_integration_iv.py +0 -0
  148. {optpricing-2.4.0 → optpricing-2.5.0}/tests/calibration/test_vectorized_pricer.py +0 -0
  149. {optpricing-2.4.0 → optpricing-2.5.0}/tests/cli/commands/test_backtest.py +0 -0
  150. {optpricing-2.4.0 → optpricing-2.5.0}/tests/cli/commands/test_calibrate.py +0 -0
  151. {optpricing-2.4.0 → optpricing-2.5.0}/tests/cli/commands/test_data.py +0 -0
  152. {optpricing-2.4.0 → optpricing-2.5.0}/tests/cli/commands/test_price.py +0 -0
  153. {optpricing-2.4.0 → optpricing-2.5.0}/tests/cli/commands/test_tools.py +0 -0
  154. {optpricing-2.4.0 → optpricing-2.5.0}/tests/cli/test_main.py +0 -0
  155. {optpricing-2.4.0 → optpricing-2.5.0}/tests/dashboard/test_plots.py +0 -0
  156. {optpricing-2.4.0 → optpricing-2.5.0}/tests/dashboard/test_service.py +0 -0
  157. {optpricing-2.4.0 → optpricing-2.5.0}/tests/dashboard/test_widgets.py +0 -0
  158. {optpricing-2.4.0 → optpricing-2.5.0}/tests/data/test_historical_manager.py +0 -0
  159. {optpricing-2.4.0 → optpricing-2.5.0}/tests/data/test_market_data_manager.py +0 -0
  160. {optpricing-2.4.0 → optpricing-2.5.0}/tests/models/base/test_base_model.py +0 -0
  161. {optpricing-2.4.0 → optpricing-2.5.0}/tests/models/base/test_validators.py +0 -0
  162. {optpricing-2.4.0 → optpricing-2.5.0}/tests/models/test_bates.py +0 -0
  163. {optpricing-2.4.0 → optpricing-2.5.0}/tests/models/test_blacks_approx.py +0 -0
  164. {optpricing-2.4.0 → optpricing-2.5.0}/tests/models/test_bsm.py +0 -0
  165. {optpricing-2.4.0 → optpricing-2.5.0}/tests/models/test_cev.py +0 -0
  166. {optpricing-2.4.0 → optpricing-2.5.0}/tests/models/test_cir.py +0 -0
  167. {optpricing-2.4.0 → optpricing-2.5.0}/tests/models/test_dupire_local.py +0 -0
  168. {optpricing-2.4.0 → optpricing-2.5.0}/tests/models/test_heston.py +0 -0
  169. {optpricing-2.4.0 → optpricing-2.5.0}/tests/models/test_hyperbolic.py +0 -0
  170. {optpricing-2.4.0 → optpricing-2.5.0}/tests/models/test_kou.py +0 -0
  171. {optpricing-2.4.0 → optpricing-2.5.0}/tests/models/test_merton_jump.py +0 -0
  172. {optpricing-2.4.0 → optpricing-2.5.0}/tests/models/test_nig.py +0 -0
  173. {optpricing-2.4.0 → optpricing-2.5.0}/tests/models/test_perpetual_put.py +0 -0
  174. {optpricing-2.4.0 → optpricing-2.5.0}/tests/models/test_vasicek.py +0 -0
  175. {optpricing-2.4.0 → optpricing-2.5.0}/tests/models/test_vg.py +0 -0
  176. {optpricing-2.4.0 → optpricing-2.5.0}/tests/parity/test_implied_rate.py +0 -0
  177. {optpricing-2.4.0 → optpricing-2.5.0}/tests/parity/test_parity_model.py +0 -0
  178. {optpricing-2.4.0 → optpricing-2.5.0}/tests/techniques/base/test_greek_mixin.py +0 -0
  179. {optpricing-2.4.0 → optpricing-2.5.0}/tests/techniques/base/test_iv_mixin.py +0 -0
  180. {optpricing-2.4.0 → optpricing-2.5.0}/tests/techniques/base/test_pricing_result.py +0 -0
  181. {optpricing-2.4.0 → optpricing-2.5.0}/tests/techniques/base/test_random_utils.py +0 -0
  182. {optpricing-2.4.0 → optpricing-2.5.0}/tests/techniques/kernels/test_lattice_kernels.py +0 -0
  183. {optpricing-2.4.0 → optpricing-2.5.0}/tests/techniques/test_american_monte_carlo.py +0 -0
  184. {optpricing-2.4.0 → optpricing-2.5.0}/tests/techniques/test_closed_form.py +0 -0
  185. {optpricing-2.4.0 → optpricing-2.5.0}/tests/techniques/test_crr.py +0 -0
  186. {optpricing-2.4.0 → optpricing-2.5.0}/tests/techniques/test_fft.py +0 -0
  187. {optpricing-2.4.0 → optpricing-2.5.0}/tests/techniques/test_integration.py +0 -0
  188. {optpricing-2.4.0 → optpricing-2.5.0}/tests/techniques/test_leisen_reimer.py +0 -0
  189. {optpricing-2.4.0 → optpricing-2.5.0}/tests/techniques/test_monte_carlo.py +0 -0
  190. {optpricing-2.4.0 → optpricing-2.5.0}/tests/techniques/test_pde.py +0 -0
  191. {optpricing-2.4.0 → optpricing-2.5.0}/tests/techniques/test_topm.py +0 -0
  192. {optpricing-2.4.0 → optpricing-2.5.0}/tests/workflows/test_backtest_workflow.py +0 -0
  193. {optpricing-2.4.0 → optpricing-2.5.0}/tests/workflows/test_daily_workflow.py +0 -0
@@ -0,0 +1,50 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions come from git tags
5
+ (`vX.Y.Z`) via setuptools_scm.
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Changed
10
+
11
+ - Python 3.10 support dropped (end of life); supported versions are 3.11–3.14, with 3.15 tested as experimental.
12
+ - Dependency pins relaxed from `~=` to verified minimum versions (`numpy>=2.3.2`, `scipy>=1.16.1`, `pandas>=2.3.3`, `numba>=0.63`, `pyyaml>=6.0.3`, `typer>=0.15.3`, `yfinance>=0.2.63`). Current releases (numpy 2.5, pandas 3.1, numba 0.68, yfinance 1.x) are supported.
13
+ - Streamlit and Plotly moved to the optional `ui` extra: `pip install "optpricing[ui]"` for the dashboard.
14
+ - Development tooling moved from the `dev` extra to PEP 735 dependency groups (`uv sync`; `pip install -e . --group dev`).
15
+ - Package metadata uses a PEP 639 license expression; the source distribution no longer bundles `data/`, `artifacts/` or docs (24 MB → ~0.1 MB).
16
+ - CI rebuilt on uv: lint, Python 3.11–3.15 matrix, minimum-dependency-versions job, build/twine checks; docs and publish workflows split into build and deploy jobs with hash-pinned actions.
17
+
18
+ ### Removed
19
+
20
+ - Unused runtime dependencies `matplotlib`, `tabulate` and the explicit `click` pin (Click arrives via Typer).
21
+
22
+ ### Fixed
23
+
24
+ - `test_price_command` relied on a hard-coded maturity date that is now in the past.
25
+ - **European Monte Carlo for Heston, Bates, SABR and SABR-jump applied the spot/variance correlation twice** (once in `MonteCarloTechnique`, once in the kernels), biasing prices by up to 2 % at ρ = −0.7. Heston MC now agrees with QuantLib's analytic engine within Monte Carlo error.
26
+ - NIG exact sampler drew the inverse-Gaussian subordinator with shape 1 instead of (δT)²; prices were ~3 % low.
27
+ - CEV exact sampler used an incorrect noncentral-χ² parametrisation (terminal mean 100.6 for a forward of 105.1), ignored the dividend yield and the seeded generator, and divided by zero at r = 0. It now samples the absorbed squared-Bessel law exactly (Makarov & Glew 2010) and matches the Cox/Schroder closed form.
28
+ - CGMY: the Lévy exponent was singular at Y = 0 and Y = 1 (Γ(−Y) poles) and the only Monte Carlo path was guarded on Y = 1 while implementing the Y = 0 law. Both limits are closed-form now; other Y sample by characteristic-function inversion.
29
+ - Hyperbolic model: characteristic function was neither the Lévy-process law nor the horizon-T hyperbolic law (so the drift was not a martingale correction for T ≠ 1), and the sampler passed α, β where scipy expects αδ, βδ. It is now the hyperbolic Lévy motion (φₜ = φ₁ᵗ) with exact sampling at T = 1 and CF inversion otherwise.
30
+ - PDE (Crank–Nicolson) Greeks were read at the grid node below the spot (delta off by ≈ 0.02); the spot now sits on a node, boundary values track the time level (with the dividend discount), and a Rannacher start-up is used.
31
+ - Longstaff–Schwartz regressed on raw powers of S through an explicit inverse of XᵀX (ill-conditioned beyond degree 2) and swallowed failures with a bare `except`; it now uses moneyness-scaled regressors and least squares.
32
+ - SABR kernels could leave the spot negative after a large down-move (puts paid more than the strike); zero is absorbing for 0 < β < 1.
33
+ - Parameter validation now rejects parameter sets without a risk-neutral measure: Kou η₁ ≤ 1, VG θν + σ²ν/2 ≥ 1, NIG/Hyperbolic |β+1| ≥ α, CGMY M ≤ 1.
34
+ - `optpricing.config` no longer writes `config.yaml` and data directories at import time (previously into `site-packages/lib` for installed packages); the root is `$OPTPRICING_HOME` or the working directory and folders are created when first written to.
35
+ - Heston and Bates read `v0` from the model parameters when it is not passed as a pricing kwarg.
36
+ - Feller-condition messages are `UserWarning`s instead of prints; pandas 3 (`inplace`, `Timestamp.utcnow`) and `importlib.resources.path` deprecations removed; `fastmath` dropped from the numba kernels (≤ 5 % measured cost, bit-identical results).
37
+
38
+ - SABR is now the four-parameter model of Hagan et al. (2002) on the forward: `alpha` is the initial volatility and the new `nu` the volatility of volatility (previously `alpha` played both roles). The old three-parameter form is still accepted with a `DeprecationWarning` (`nu = alpha`). Monte Carlo simulates the forward dynamics (prices move by O((r−q)T(1−β)) relative to the old spot-with-drift kernel) and the SABR kernels' `alpha` argument is renamed `nu`.
39
+ - Heston and Bates default parameters now satisfy the Feller condition (`vol_of_vol` 0.5 → 0.3).
40
+
41
+ ### Added
42
+
43
+ - `SABRModel.implied_volatility` (Hagan's lognormal expansion, bit-identical to QuantLib's `sabrVolatility`), an optional `shift` parameter for shifted SABR, and a Black-76 closed form so `ClosedFormTechnique`, the CLI and calibration price SABR analytically.
44
+ - `benchmarks/run_benchmarks.py` with recorded before/after results, and a reference-value test suite (`tests/correctness/`) pinned to QuantLib 1.44 and textbook closed forms.
45
+
46
+ ## [2.0.0] - 2025-06-26
47
+
48
+ ### Added
49
+
50
+ - Shipped to PyPI `pyproject.toml`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: optpricing
3
- Version: 2.4.0
3
+ Version: 2.5.0
4
4
  Summary: A financial options pricing and analysis library.
5
5
  Author: Diljit Singh
6
6
  License-Expression: MIT
@@ -0,0 +1,164 @@
1
+ """Micro-benchmarks for the core pricing paths.
2
+
3
+ Usage (from the repository root)::
4
+
5
+ uv run python benchmarks/run_benchmarks.py # print table
6
+ uv run python benchmarks/run_benchmarks.py --out results/x.json
7
+ uv run python benchmarks/run_benchmarks.py --repeats 9 --only mc_heston_100k
8
+
9
+ Each case is warmed up once (numba compilation, caches) and then timed
10
+ ``repeats`` times; the median and minimum wall-clock times are reported.
11
+ Numbers are only comparable on the same machine, so results files record the
12
+ platform, interpreter and library versions alongside the git revision.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import platform
20
+ import statistics
21
+ import subprocess
22
+ import sys
23
+ import time
24
+ from collections.abc import Callable
25
+ from datetime import UTC, datetime
26
+ from pathlib import Path
27
+
28
+ import numba
29
+ import numpy as np
30
+ import scipy
31
+
32
+ from optpricing import BSMModel, HestonModel, Option, OptionType, Rate, Stock
33
+ from optpricing.techniques import (
34
+ AmericanMonteCarloTechnique,
35
+ ClosedFormTechnique,
36
+ FFTTechnique,
37
+ IntegrationTechnique,
38
+ MonteCarloTechnique,
39
+ PDETechnique,
40
+ )
41
+
42
+ OPTION = Option(strike=100.0, maturity=1.0, option_type=OptionType.CALL)
43
+ PUT = Option(strike=100.0, maturity=1.0, option_type=OptionType.PUT)
44
+ STOCK = Stock(spot=100.0, dividend=0.01)
45
+ RATE = Rate(rate=0.05)
46
+ BSM = BSMModel(params={"sigma": 0.2})
47
+ HESTON = HestonModel(
48
+ params={"v0": 0.04, "kappa": 2.0, "theta": 0.05, "rho": -0.7, "vol_of_vol": 0.5}
49
+ )
50
+
51
+
52
+ def _bsm_closed_form() -> float:
53
+ tech = ClosedFormTechnique()
54
+ for _ in range(1000):
55
+ p = tech.price(OPTION, STOCK, BSM, RATE).price
56
+ return p
57
+
58
+
59
+ def _heston_fft() -> float:
60
+ return FFTTechnique().price(OPTION, STOCK, HESTON, RATE, v0=0.04).price
61
+
62
+
63
+ def _heston_integration() -> float:
64
+ return IntegrationTechnique().price(OPTION, STOCK, HESTON, RATE, v0=0.04).price
65
+
66
+
67
+ def _cn_pde() -> float:
68
+ return PDETechnique(M=200, N=200).price(OPTION, STOCK, BSM, RATE).price
69
+
70
+
71
+ def _mc_bsm_100k() -> float:
72
+ mc = MonteCarloTechnique(n_paths=100_000, n_steps=100, seed=1)
73
+ return mc.price(OPTION, STOCK, BSM, RATE).price
74
+
75
+
76
+ def _mc_heston_100k() -> float:
77
+ mc = MonteCarloTechnique(n_paths=100_000, n_steps=100, seed=1)
78
+ return mc.price(OPTION, STOCK, HESTON, RATE, v0=0.04).price
79
+
80
+
81
+ def _lsmc_bsm_20k() -> float:
82
+ amc = AmericanMonteCarloTechnique(n_paths=20_000, n_steps=50, seed=1)
83
+ return amc.price(PUT, STOCK, BSM, RATE).price
84
+
85
+
86
+ CASES: dict[str, tuple[Callable[[], float], str]] = {
87
+ "bsm_closed_form_x1000": (_bsm_closed_form, "1000 closed-form BSM prices"),
88
+ "heston_fft": (_heston_fft, "Carr-Madan FFT, N=4096"),
89
+ "heston_integration": (_heston_integration, "Gil-Pelaez quadrature"),
90
+ "cn_pde_200x200": (_cn_pde, "Crank-Nicolson, 200 x 200 grid"),
91
+ "mc_bsm_100k": (_mc_bsm_100k, "100k paths x 100 steps, antithetic"),
92
+ "mc_heston_100k": (_mc_heston_100k, "100k paths x 100 steps, antithetic"),
93
+ "lsmc_bsm_20k": (_lsmc_bsm_20k, "Longstaff-Schwartz, 20k paths x 50 steps"),
94
+ }
95
+
96
+
97
+ def _git_rev() -> str:
98
+ try:
99
+ rev = subprocess.run(
100
+ ["git", "rev-parse", "--short", "HEAD"],
101
+ capture_output=True,
102
+ text=True,
103
+ check=True,
104
+ ).stdout.strip()
105
+ dirty = subprocess.run(
106
+ ["git", "status", "--porcelain"], capture_output=True, text=True, check=True
107
+ ).stdout.strip()
108
+ return rev + ("-dirty" if dirty else "")
109
+ except Exception: # pragma: no cover - git may be unavailable
110
+ return "unknown"
111
+
112
+
113
+ def run(repeats: int, only: list[str] | None) -> dict:
114
+ results = {}
115
+ for name, (fn, desc) in CASES.items():
116
+ if only and name not in only:
117
+ continue
118
+ value = fn() # warm-up: numba compile, lazy imports
119
+ times = []
120
+ for _ in range(repeats):
121
+ t0 = time.perf_counter()
122
+ fn()
123
+ times.append(time.perf_counter() - t0)
124
+ results[name] = {
125
+ "description": desc,
126
+ "median_ms": 1e3 * statistics.median(times),
127
+ "min_ms": 1e3 * min(times),
128
+ "repeats": repeats,
129
+ "value": float(value),
130
+ }
131
+ return results
132
+
133
+
134
+ def main() -> None:
135
+ parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
136
+ parser.add_argument("--repeats", type=int, default=5)
137
+ parser.add_argument("--only", nargs="*", help="subset of case names")
138
+ parser.add_argument("--out", type=Path, help="write JSON results to this path")
139
+ args = parser.parse_args()
140
+
141
+ results = run(args.repeats, args.only)
142
+ payload = {
143
+ "timestamp": datetime.now(UTC).isoformat(timespec="seconds"),
144
+ "git": _git_rev(),
145
+ "platform": platform.platform(),
146
+ "machine": platform.machine(),
147
+ "python": sys.version.split()[0],
148
+ "numpy": np.__version__,
149
+ "scipy": scipy.__version__,
150
+ "numba": numba.__version__,
151
+ "results": results,
152
+ }
153
+
154
+ print(f"{'case':26s}{'median ms':>12s}{'min ms':>10s} value")
155
+ for name, r in results.items():
156
+ print(f"{name:26s}{r['median_ms']:12.3f}{r['min_ms']:10.3f} {r['value']:.6f}")
157
+ if args.out:
158
+ args.out.parent.mkdir(parents=True, exist_ok=True)
159
+ args.out.write_text(json.dumps(payload, indent=2) + "\n")
160
+ print(f"\nwrote {args.out}")
161
+
162
+
163
+ if __name__ == "__main__":
164
+ main()
@@ -135,6 +135,10 @@ testpaths = ["tests"]
135
135
  # Coverage is opt-in: `pytest --cov` (CI does this); plain `pytest` stays fast and debuggable.
136
136
  addopts = "-ra --strict-markers --strict-config"
137
137
  xfail_strict = true
138
+ filterwarnings = [
139
+ # Several fixtures deliberately use Feller-violating Heston/CIR parameters.
140
+ "ignore:.*do not satisfy the Feller condition.*:UserWarning",
141
+ ]
138
142
 
139
143
  [tool.coverage.run]
140
144
  source = ["optpricing"]
@@ -95,7 +95,7 @@ class VolatilitySurface:
95
95
  market_ivs = self._calculate_ivs(stock, rate, self.data["marketPrice"])
96
96
  self.surface = self.data.copy()
97
97
  self.surface["iv"] = market_ivs
98
- self.surface.dropna(inplace=True)
98
+ self.surface = self.surface.dropna()
99
99
  self.surface = self.surface[
100
100
  (self.surface["iv"] > 1e-4) & (self.surface["iv"] < 2.0)
101
101
  ]
@@ -146,5 +146,5 @@ class VolatilitySurface:
146
146
  )
147
147
  self.surface = self.data.copy()
148
148
  self.surface["iv"] = model_ivs
149
- self.surface.dropna(inplace=True)
149
+ self.surface = self.surface.dropna()
150
150
  return self
@@ -15,7 +15,8 @@ def dashboard():
15
15
  Launches the Streamlit dashboard application.
16
16
  """
17
17
  try:
18
- with resources.path("optpricing.dashboard", "Home.py") as app_path:
18
+ home = resources.files("optpricing.dashboard").joinpath("Home.py")
19
+ with resources.as_file(home) as app_path:
19
20
  typer.echo(f"Launching Streamlit dashboard from: {app_path}")
20
21
  subprocess.run(["streamlit", "run", str(app_path)], check=True)
21
22
  except FileNotFoundError:
@@ -172,7 +172,7 @@ def price(
172
172
  stock = Stock(spot=spot, dividend=q_div)
173
173
  rate = Rate(rate=r_rate)
174
174
  maturity_years = (
175
- pd.to_datetime(maturity) - pd.Timestamp.utcnow().tz_localize(None)
175
+ pd.to_datetime(maturity) - pd.Timestamp.now(tz="UTC").tz_localize(None)
176
176
  ).days / 365.25
177
177
  if maturity_years <= 0:
178
178
  _err("Maturity date must be in the future.")
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ import yaml
7
+
8
+ __doc__ = """
9
+ Resolve the project root, read the optional ``config.yaml`` and expose the data
10
+ and artifact directories.
11
+
12
+ The root is ``$OPTPRICING_HOME`` if set, otherwise the current working directory.
13
+ Running from a source checkout therefore behaves as before, while an installed
14
+ package no longer writes ``config.yaml`` or data folders next to its own modules
15
+ in ``site-packages``. Nothing is created at import time; writers create the
16
+ directory they need (see :func:`ensure_directories`).
17
+ """
18
+
19
+ DEFAULT_CONFIG: dict[str, object] = {
20
+ "live_data_provider": "yfinance",
21
+ "data_directory": "data",
22
+ "artifacts_directory": "artifacts",
23
+ }
24
+
25
+ PROJECT_ROOT = Path(os.environ.get("OPTPRICING_HOME", Path.cwd())).resolve()
26
+ CONFIG_FILE_PATH = PROJECT_ROOT / "config.yaml"
27
+
28
+
29
+ def _load_config(path: Path) -> dict:
30
+ if not path.exists():
31
+ return dict(DEFAULT_CONFIG)
32
+ with open(path) as f:
33
+ loaded = yaml.safe_load(f) or {}
34
+ return {**DEFAULT_CONFIG, **loaded}
35
+
36
+
37
+ _config = _load_config(CONFIG_FILE_PATH)
38
+
39
+ # Directory Management
40
+ DATA_DIR = PROJECT_ROOT / str(_config.get("data_directory", "data"))
41
+ ARTIFACTS_DIR = PROJECT_ROOT / str(_config.get("artifacts_directory", "artifacts"))
42
+
43
+ # Input Data Directories
44
+ MARKET_SNAPSHOT_DIR = DATA_DIR / "market_data_snapshots"
45
+ HISTORICAL_DIR = DATA_DIR / "historical_data"
46
+
47
+ # Output Artifact Directories
48
+ BACKTEST_LOGS_DIR = ARTIFACTS_DIR / "backtest_logs"
49
+ CALIBRATED_PARAMS_DIR = ARTIFACTS_DIR / "calibrated_params"
50
+ CALIBRATION_LOGS_DIR = ARTIFACTS_DIR / "calibration_logs"
51
+
52
+ ALL_DIRS = [
53
+ DATA_DIR,
54
+ ARTIFACTS_DIR,
55
+ MARKET_SNAPSHOT_DIR,
56
+ HISTORICAL_DIR,
57
+ BACKTEST_LOGS_DIR,
58
+ CALIBRATED_PARAMS_DIR,
59
+ CALIBRATION_LOGS_DIR,
60
+ ]
61
+
62
+
63
+ def ensure_directories() -> None:
64
+ """Create all data and artifact directories (idempotent)."""
65
+ for path in ALL_DIRS:
66
+ path.mkdir(parents=True, exist_ok=True)
@@ -42,6 +42,7 @@ def save_historical_returns(
42
42
 
43
43
  log_returns = np.log(data["Close"] / data["Close"].shift(1)).dropna()
44
44
  filename = HISTORICAL_DIR / f"{ticker}_{period}_returns.parquet"
45
+ filename.parent.mkdir(parents=True, exist_ok=True)
45
46
  log_returns.to_frame(name="log_return").to_parquet(filename)
46
47
 
47
48
  print(f" -> Saved to {filename}")
@@ -45,9 +45,8 @@ def _fetch_from_yfinance(ticker: str) -> pd.DataFrame | None:
45
45
  "last_price", ticker_obj.history("1d")["Close"].iloc[0]
46
46
  )
47
47
 
48
- chain_df.dropna(
48
+ chain_df = chain_df.dropna(
49
49
  subset=["marketPrice", "strike", "maturity", "impliedVolatility"],
50
- inplace=True,
51
50
  )
52
51
  chain_df = chain_df[
53
52
  (chain_df["marketPrice"] > 0.01) & (chain_df["maturity"] > 1 / 365.25)
@@ -112,6 +111,7 @@ def save_market_snapshot(tickers: list[str]):
112
111
  continue
113
112
 
114
113
  filename = MARKET_SNAPSHOT_DIR / f"{ticker}_{today_str}.parquet"
114
+ filename.parent.mkdir(parents=True, exist_ok=True)
115
115
  chain_df.to_parquet(filename)
116
116
  print(f" -> Successfully saved {len(chain_df)} options to {filename}")
117
117
 
@@ -7,6 +7,7 @@ parameter validation tools.
7
7
  """
8
8
 
9
9
  from .base_model import CF, BaseModel, PDECoeffs
10
+ from .cf_sampler import sample_from_cf
10
11
  from .validators import ParamValidator
11
12
 
12
13
  __all__ = [
@@ -14,4 +15,5 @@ __all__ = [
14
15
  "CF",
15
16
  "PDECoeffs",
16
17
  "ParamValidator",
18
+ "sample_from_cf",
17
19
  ]
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+
5
+ import numpy as np
6
+
7
+ __doc__ = """
8
+ Inverse-transform sampling of a univariate law from its characteristic function.
9
+
10
+ Used by pure-jump Levy models whose terminal distribution has no tractable exact
11
+ sampler at an arbitrary horizon (CGMY for general Y, the hyperbolic Levy motion
12
+ for t != 1). The density is recovered with the COS expansion of Fang & Oosterlee
13
+ (2008) on a cumulant-based truncation range, integrated to a CDF on a fine grid,
14
+ and inverted by interpolation.
15
+ """
16
+
17
+
18
+ def _cumulants_from_cf(cf: Callable, h: float = 1e-3) -> tuple[float, float]:
19
+ """First two cumulants from central differences of the CF at the origin."""
20
+ phi_p, phi_m = cf(h), cf(-h)
21
+ c1 = float(np.imag(phi_p - phi_m) / (2.0 * h))
22
+ second_moment = -float(np.real(phi_p - 2.0 + phi_m) / h**2)
23
+ c2 = max(second_moment - c1**2, 1e-16)
24
+ return c1, c2
25
+
26
+
27
+ def cos_cdf_table(
28
+ cf: Callable,
29
+ *,
30
+ n_terms: int = 4096,
31
+ n_grid: int = 8193,
32
+ width: float = 12.0,
33
+ ) -> tuple[np.ndarray, np.ndarray]:
34
+ """
35
+ Tabulate the CDF implied by a characteristic function on a regular grid.
36
+
37
+ Parameters
38
+ ----------
39
+ cf : Callable
40
+ Characteristic function ``u -> E[exp(iuX)]`` accepting numpy arrays.
41
+ n_terms : int, optional
42
+ Number of COS terms, by default 4096.
43
+ n_grid : int, optional
44
+ Number of grid points for the tabulated CDF, by default 8193.
45
+ width : float, optional
46
+ Half-width of the truncation range in standard deviations, by default 12.
47
+ The range is widened automatically if it does not capture the mass.
48
+
49
+ Returns
50
+ -------
51
+ tuple[np.ndarray, np.ndarray]
52
+ ``(x_grid, cdf)`` with ``cdf`` non-decreasing from 0 to 1.
53
+ """
54
+ c1, c2 = _cumulants_from_cf(cf)
55
+ std = np.sqrt(c2)
56
+
57
+ for _ in range(6):
58
+ a, b = c1 - width * std, c1 + width * std
59
+ span = b - a
60
+ k = np.arange(n_terms)
61
+ omega = k * np.pi / span
62
+ coeffs = (2.0 / span) * np.real(cf(omega) * np.exp(-1j * omega * a))
63
+ coeffs[0] *= 0.5
64
+
65
+ x = np.linspace(a, b, n_grid)
66
+ # CDF(x) = int_a^x density
67
+ # = c_0 (x-a) + sum_{k>=1} c_k (span/(k pi)) sin(k pi (x-a)/span)
68
+ phase = np.outer(x - a, omega[1:])
69
+ sine_weights = coeffs[1:] * span / (k[1:] * np.pi)
70
+ cdf = coeffs[0] * (x - a) + np.sin(phase) @ sine_weights
71
+
72
+ total = cdf[-1]
73
+ edge_mass = max(abs(cdf[1] - cdf[0]), abs(cdf[-1] - cdf[-2])) / (x[1] - x[0])
74
+ if abs(total - 1.0) < 1e-6 and edge_mass * std < 1e-7:
75
+ break
76
+ width *= 1.5 # heavy tails: widen and retry
77
+
78
+ cdf = np.maximum.accumulate(np.clip(cdf, 0.0, None))
79
+ cdf /= cdf[-1]
80
+ return x, cdf
81
+
82
+
83
+ def sample_from_cf(
84
+ cf: Callable,
85
+ size: int,
86
+ rng: np.random.Generator,
87
+ **table_kwargs,
88
+ ) -> np.ndarray:
89
+ """
90
+ Draw ``size`` samples of the law with characteristic function ``cf``.
91
+
92
+ Parameters
93
+ ----------
94
+ cf : Callable
95
+ Characteristic function accepting numpy arrays.
96
+ size : int
97
+ Number of samples.
98
+ rng : np.random.Generator
99
+ Random number generator used for the uniforms.
100
+ **table_kwargs
101
+ Forwarded to :func:`cos_cdf_table`.
102
+
103
+ Returns
104
+ -------
105
+ np.ndarray
106
+ Samples obtained by inverse transform of the tabulated CDF.
107
+ """
108
+ x, cdf = cos_cdf_table(cf, **table_kwargs)
109
+ u = rng.uniform(0.0, 1.0, size=size)
110
+ return np.interp(u, cdf, x)
@@ -35,7 +35,7 @@ class BatesModel(BaseModel):
35
35
  "kappa": 2.0,
36
36
  "theta": 0.04,
37
37
  "rho": -0.7,
38
- "vol_of_vol": 0.5,
38
+ "vol_of_vol": 0.3,
39
39
  "lambda": 0.5,
40
40
  "mu_j": -0.1,
41
41
  "sigma_j": 0.15,
@@ -78,7 +78,7 @@ class BatesModel(BaseModel):
78
78
  spot: float,
79
79
  r: float,
80
80
  q: float,
81
- v0: float,
81
+ v0: float | None = None,
82
82
  **_: Any,
83
83
  ) -> CF:
84
84
  """
@@ -94,8 +94,9 @@ class BatesModel(BaseModel):
94
94
  The continuously compounded risk-free rate.
95
95
  q : float
96
96
  The continuously compounded dividend yield.
97
- v0 : float
98
- The initial variance of the asset's returns.
97
+ v0 : float | None, optional
98
+ The initial variance of the asset's returns. Defaults to the model's
99
+ ``v0`` parameter.
99
100
 
100
101
  Returns
101
102
  -------
@@ -103,6 +104,12 @@ class BatesModel(BaseModel):
103
104
  The characteristic function.
104
105
  """
105
106
  p = self.params
107
+ if v0 is None:
108
+ v0 = p.get("v0")
109
+ if v0 is None:
110
+ raise ValueError(
111
+ f"{self.name}: 'v0' must be a model parameter or a pricing kwarg."
112
+ )
106
113
  kappa, theta, rho, vol_of_vol = (
107
114
  p["kappa"],
108
115
  p["theta"],