optPricing 2.5.0__tar.gz → 2.6.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 (194) hide show
  1. {optpricing-2.5.0 → optpricing-2.6.0}/CHANGELOG.md +22 -0
  2. {optpricing-2.5.0/src/optpricing.egg-info → optpricing-2.6.0}/PKG-INFO +1 -1
  3. {optpricing-2.5.0 → optpricing-2.6.0}/benchmarks/run_benchmarks.py +20 -0
  4. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/calibration/vectorized_bsm_iv.py +9 -8
  5. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/calibration/vectorized_pricer.py +8 -10
  6. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/bsm.py +19 -12
  7. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/merton_jump.py +33 -21
  8. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/sabr.py +3 -3
  9. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/techniques/american_monte_carlo.py +73 -51
  10. optpricing-2.6.0/src/optpricing/techniques/base/random_utils.py +149 -0
  11. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/techniques/fft.py +25 -4
  12. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/techniques/integration.py +76 -27
  13. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/techniques/kernels/__init__.py +8 -0
  14. optpricing-2.6.0/src/optpricing/techniques/kernels/american_mc_kernels.py +143 -0
  15. optpricing-2.6.0/src/optpricing/techniques/kernels/mc_kernels.py +470 -0
  16. optpricing-2.6.0/src/optpricing/techniques/kernels/path_kernels.py +425 -0
  17. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/techniques/monte_carlo.py +75 -63
  18. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/techniques/pde.py +82 -27
  19. {optpricing-2.5.0 → optpricing-2.6.0/src/optpricing.egg-info}/PKG-INFO +1 -1
  20. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing.egg-info/scm_version.json +2 -2
  21. {optpricing-2.5.0 → optpricing-2.6.0}/tests/correctness/test_mc_regressions.py +74 -34
  22. optpricing-2.6.0/tests/techniques/kernels/test_mc_kernels.py +178 -0
  23. optpricing-2.6.0/tests/techniques/kernels/test_path_kernels.py +93 -0
  24. {optpricing-2.5.0 → optpricing-2.6.0}/tests/techniques/test_american_monte_carlo.py +5 -2
  25. {optpricing-2.5.0 → optpricing-2.6.0}/tests/techniques/test_monte_carlo.py +14 -4
  26. optpricing-2.5.0/src/optpricing/techniques/base/random_utils.py +0 -31
  27. optpricing-2.5.0/src/optpricing/techniques/kernels/american_mc_kernels.py +0 -85
  28. optpricing-2.5.0/src/optpricing/techniques/kernels/mc_kernels.py +0 -367
  29. optpricing-2.5.0/src/optpricing/techniques/kernels/path_kernels.py +0 -334
  30. optpricing-2.5.0/tests/techniques/kernels/test_mc_kernels.py +0 -479
  31. optpricing-2.5.0/tests/techniques/kernels/test_path_kernels.py +0 -289
  32. {optpricing-2.5.0 → optpricing-2.6.0}/LICENSE +0 -0
  33. {optpricing-2.5.0 → optpricing-2.6.0}/MANIFEST.in +0 -0
  34. {optpricing-2.5.0 → optpricing-2.6.0}/README.md +0 -0
  35. {optpricing-2.5.0 → optpricing-2.6.0}/pyproject.toml +0 -0
  36. {optpricing-2.5.0 → optpricing-2.6.0}/setup.cfg +0 -0
  37. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/__init__.py +0 -0
  38. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/atoms/__init__.py +0 -0
  39. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/atoms/bond.py +0 -0
  40. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/atoms/option.py +0 -0
  41. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/atoms/rate.py +0 -0
  42. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/atoms/stock.py +0 -0
  43. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/calibration/__init__.py +0 -0
  44. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/calibration/calibrator.py +0 -0
  45. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/calibration/fit_jump_parameters.py +0 -0
  46. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/calibration/fit_market_params.py +0 -0
  47. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/calibration/iv_surface.py +0 -0
  48. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/calibration/technique_selector.py +0 -0
  49. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/calibration/vectorized_integration_iv.py +0 -0
  50. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/cli/__init__.py +0 -0
  51. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/cli/commands/__init__.py +0 -0
  52. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/cli/commands/backtest.py +0 -0
  53. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/cli/commands/calibrate.py +0 -0
  54. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/cli/commands/dashboard.py +0 -0
  55. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/cli/commands/data.py +0 -0
  56. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/cli/commands/demo.py +0 -0
  57. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/cli/commands/price.py +0 -0
  58. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/cli/commands/tools.py +0 -0
  59. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/cli/main.py +0 -0
  60. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/config.py +0 -0
  61. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/dashboard/Home.py +0 -0
  62. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/dashboard/__init__.py +0 -0
  63. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/dashboard/pages/1_Pricer_and_Greeks.py +0 -0
  64. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/dashboard/pages/2_Calibration_and_IV.py +0 -0
  65. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/dashboard/pages/3_Market_Analytics.py +0 -0
  66. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/dashboard/pages/4_Model_Fitting.py +0 -0
  67. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/dashboard/pages/5_Term_Structure.py +0 -0
  68. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/dashboard/pages/__init__.py +0 -0
  69. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/dashboard/plots.py +0 -0
  70. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/dashboard/service.py +0 -0
  71. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/dashboard/widgets.py +0 -0
  72. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/data/__init__.py +0 -0
  73. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/data/historical_manager.py +0 -0
  74. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/data/market_data_manager.py +0 -0
  75. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/__init__.py +0 -0
  76. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/base/__init__.py +0 -0
  77. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/base/base_model.py +0 -0
  78. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/base/cf_sampler.py +0 -0
  79. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/base/validators.py +0 -0
  80. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/bates.py +0 -0
  81. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/blacks_approx.py +0 -0
  82. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/cev.py +0 -0
  83. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/cgmy.py +0 -0
  84. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/cir.py +0 -0
  85. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/dupire_local.py +0 -0
  86. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/heston.py +0 -0
  87. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/hyperbolic.py +0 -0
  88. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/kou.py +0 -0
  89. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/nig.py +0 -0
  90. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/perpetual_put.py +0 -0
  91. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/sabr_jump.py +0 -0
  92. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/vasicek.py +0 -0
  93. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/models/vg.py +0 -0
  94. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/parity/__init__.py +0 -0
  95. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/parity/implied_rate.py +0 -0
  96. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/parity/parity_model.py +0 -0
  97. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/techniques/__init__.py +0 -0
  98. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/techniques/base/__init__.py +0 -0
  99. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/techniques/base/base_technique.py +0 -0
  100. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/techniques/base/greek_mixin.py +0 -0
  101. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/techniques/base/iv_mixin.py +0 -0
  102. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/techniques/base/lattice_technique.py +0 -0
  103. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/techniques/base/pricing_result.py +0 -0
  104. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/techniques/closed_form.py +0 -0
  105. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/techniques/crr.py +0 -0
  106. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/techniques/kernels/lattice_kernels.py +0 -0
  107. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/techniques/leisen_reimer.py +0 -0
  108. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/techniques/topm.py +0 -0
  109. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/workflows/__init__.py +0 -0
  110. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/workflows/backtest_workflow.py +0 -0
  111. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/workflows/configs/__init__.py +0 -0
  112. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/workflows/configs/bates_config.py +0 -0
  113. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/workflows/configs/bsm_config.py +0 -0
  114. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/workflows/configs/cev_config.py +0 -0
  115. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/workflows/configs/cgmy_config.py +0 -0
  116. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/workflows/configs/heston_config.py +0 -0
  117. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/workflows/configs/hyperbolic_config.py +0 -0
  118. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/workflows/configs/kou_config.py +0 -0
  119. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/workflows/configs/merton_config.py +0 -0
  120. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/workflows/configs/nig_config.py +0 -0
  121. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/workflows/configs/sabr_config.py +0 -0
  122. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/workflows/configs/sabr_jump_config.py +0 -0
  123. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/workflows/configs/vg_config.py +0 -0
  124. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing/workflows/daily_workflow.py +0 -0
  125. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing.egg-info/SOURCES.txt +0 -0
  126. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing.egg-info/dependency_links.txt +0 -0
  127. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing.egg-info/entry_points.txt +0 -0
  128. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing.egg-info/requires.txt +0 -0
  129. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing.egg-info/scm_file_list.json +0 -0
  130. {optpricing-2.5.0 → optpricing-2.6.0}/src/optpricing.egg-info/top_level.txt +0 -0
  131. {optpricing-2.5.0 → optpricing-2.6.0}/tests/atoms/test_bond.py +0 -0
  132. {optpricing-2.5.0 → optpricing-2.6.0}/tests/atoms/test_option.py +0 -0
  133. {optpricing-2.5.0 → optpricing-2.6.0}/tests/atoms/test_rate.py +0 -0
  134. {optpricing-2.5.0 → optpricing-2.6.0}/tests/atoms/test_stock.py +0 -0
  135. {optpricing-2.5.0 → optpricing-2.6.0}/tests/calibration/test_calibrator.py +0 -0
  136. {optpricing-2.5.0 → optpricing-2.6.0}/tests/calibration/test_fit_jump_parameters.py +0 -0
  137. {optpricing-2.5.0 → optpricing-2.6.0}/tests/calibration/test_fit_market_params.py +0 -0
  138. {optpricing-2.5.0 → optpricing-2.6.0}/tests/calibration/test_iv_surface.py +0 -0
  139. {optpricing-2.5.0 → optpricing-2.6.0}/tests/calibration/test_technique_selector.py +0 -0
  140. {optpricing-2.5.0 → optpricing-2.6.0}/tests/calibration/test_vectorized_bsm_iv.py +0 -0
  141. {optpricing-2.5.0 → optpricing-2.6.0}/tests/calibration/test_vectorized_integration_iv.py +0 -0
  142. {optpricing-2.5.0 → optpricing-2.6.0}/tests/calibration/test_vectorized_pricer.py +0 -0
  143. {optpricing-2.5.0 → optpricing-2.6.0}/tests/cli/commands/test_backtest.py +0 -0
  144. {optpricing-2.5.0 → optpricing-2.6.0}/tests/cli/commands/test_calibrate.py +0 -0
  145. {optpricing-2.5.0 → optpricing-2.6.0}/tests/cli/commands/test_data.py +0 -0
  146. {optpricing-2.5.0 → optpricing-2.6.0}/tests/cli/commands/test_price.py +0 -0
  147. {optpricing-2.5.0 → optpricing-2.6.0}/tests/cli/commands/test_tools.py +0 -0
  148. {optpricing-2.5.0 → optpricing-2.6.0}/tests/cli/test_main.py +0 -0
  149. {optpricing-2.5.0 → optpricing-2.6.0}/tests/correctness/__init__.py +0 -0
  150. {optpricing-2.5.0 → optpricing-2.6.0}/tests/correctness/_helpers.py +0 -0
  151. {optpricing-2.5.0 → optpricing-2.6.0}/tests/correctness/conftest.py +0 -0
  152. {optpricing-2.5.0 → optpricing-2.6.0}/tests/correctness/test_invariants.py +0 -0
  153. {optpricing-2.5.0 → optpricing-2.6.0}/tests/correctness/test_reference_values.py +0 -0
  154. {optpricing-2.5.0 → optpricing-2.6.0}/tests/dashboard/test_plots.py +0 -0
  155. {optpricing-2.5.0 → optpricing-2.6.0}/tests/dashboard/test_service.py +0 -0
  156. {optpricing-2.5.0 → optpricing-2.6.0}/tests/dashboard/test_widgets.py +0 -0
  157. {optpricing-2.5.0 → optpricing-2.6.0}/tests/data/test_historical_manager.py +0 -0
  158. {optpricing-2.5.0 → optpricing-2.6.0}/tests/data/test_market_data_manager.py +0 -0
  159. {optpricing-2.5.0 → optpricing-2.6.0}/tests/models/base/test_base_model.py +0 -0
  160. {optpricing-2.5.0 → optpricing-2.6.0}/tests/models/base/test_validators.py +0 -0
  161. {optpricing-2.5.0 → optpricing-2.6.0}/tests/models/test_bates.py +0 -0
  162. {optpricing-2.5.0 → optpricing-2.6.0}/tests/models/test_blacks_approx.py +0 -0
  163. {optpricing-2.5.0 → optpricing-2.6.0}/tests/models/test_bsm.py +0 -0
  164. {optpricing-2.5.0 → optpricing-2.6.0}/tests/models/test_cev.py +0 -0
  165. {optpricing-2.5.0 → optpricing-2.6.0}/tests/models/test_cgmy.py +0 -0
  166. {optpricing-2.5.0 → optpricing-2.6.0}/tests/models/test_cir.py +0 -0
  167. {optpricing-2.5.0 → optpricing-2.6.0}/tests/models/test_dupire_local.py +0 -0
  168. {optpricing-2.5.0 → optpricing-2.6.0}/tests/models/test_heston.py +0 -0
  169. {optpricing-2.5.0 → optpricing-2.6.0}/tests/models/test_hyperbolic.py +0 -0
  170. {optpricing-2.5.0 → optpricing-2.6.0}/tests/models/test_kou.py +0 -0
  171. {optpricing-2.5.0 → optpricing-2.6.0}/tests/models/test_merton_jump.py +0 -0
  172. {optpricing-2.5.0 → optpricing-2.6.0}/tests/models/test_nig.py +0 -0
  173. {optpricing-2.5.0 → optpricing-2.6.0}/tests/models/test_perpetual_put.py +0 -0
  174. {optpricing-2.5.0 → optpricing-2.6.0}/tests/models/test_sabr.py +0 -0
  175. {optpricing-2.5.0 → optpricing-2.6.0}/tests/models/test_sabr_jump.py +0 -0
  176. {optpricing-2.5.0 → optpricing-2.6.0}/tests/models/test_vasicek.py +0 -0
  177. {optpricing-2.5.0 → optpricing-2.6.0}/tests/models/test_vg.py +0 -0
  178. {optpricing-2.5.0 → optpricing-2.6.0}/tests/parity/test_implied_rate.py +0 -0
  179. {optpricing-2.5.0 → optpricing-2.6.0}/tests/parity/test_parity_model.py +0 -0
  180. {optpricing-2.5.0 → optpricing-2.6.0}/tests/techniques/base/test_greek_mixin.py +0 -0
  181. {optpricing-2.5.0 → optpricing-2.6.0}/tests/techniques/base/test_iv_mixin.py +0 -0
  182. {optpricing-2.5.0 → optpricing-2.6.0}/tests/techniques/base/test_pricing_result.py +0 -0
  183. {optpricing-2.5.0 → optpricing-2.6.0}/tests/techniques/base/test_random_utils.py +0 -0
  184. {optpricing-2.5.0 → optpricing-2.6.0}/tests/techniques/kernels/test_lattice_kernels.py +0 -0
  185. {optpricing-2.5.0 → optpricing-2.6.0}/tests/techniques/test_closed_form.py +0 -0
  186. {optpricing-2.5.0 → optpricing-2.6.0}/tests/techniques/test_crr.py +0 -0
  187. {optpricing-2.5.0 → optpricing-2.6.0}/tests/techniques/test_fft.py +0 -0
  188. {optpricing-2.5.0 → optpricing-2.6.0}/tests/techniques/test_integration.py +0 -0
  189. {optpricing-2.5.0 → optpricing-2.6.0}/tests/techniques/test_leisen_reimer.py +0 -0
  190. {optpricing-2.5.0 → optpricing-2.6.0}/tests/techniques/test_pde.py +0 -0
  191. {optpricing-2.5.0 → optpricing-2.6.0}/tests/techniques/test_topm.py +0 -0
  192. {optpricing-2.5.0 → optpricing-2.6.0}/tests/test_config.py +0 -0
  193. {optpricing-2.5.0 → optpricing-2.6.0}/tests/workflows/test_backtest_workflow.py +0 -0
  194. {optpricing-2.5.0 → optpricing-2.6.0}/tests/workflows/test_daily_workflow.py +0 -0
@@ -6,6 +6,28 @@ All notable changes to this project are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ### Performance
10
+
11
+ - Monte Carlo engine rebuilt: the numba kernels draw their own random numbers from per-chunk
12
+ `numpy.random.Generator`s (spawned from one seed, fixed chunk size) and run concurrently on a thread pool
13
+ (`n_jobs`, `chunk_size` options; `OPTPRICING_MC_THREADS`). Results depend only on the seed and chunk size,
14
+ common-random-number Greeks are preserved, and jump sizes now come from the seeded stream. 100k-path
15
+ BSM 27.7 → 3.4 ms, Heston (Euler) 88.7 → 7.5 ms on 10 cores.
16
+ - Heston/Bates Monte Carlo defaults to Andersen's Quadratic-Exponential variance scheme with martingale
17
+ correction (`variance_scheme="qe"`; `"euler"` keeps full truncation). 20 QE steps are more accurate than
18
+ 252 Euler steps; on long-dated, high vol-of-vol parameters Euler was off by several price units.
19
+ - Closed forms use `scipy.special.ndtr` (BSM price 28 → 2 µs); the Merton series is vectorised with log-space
20
+ Poisson weights (2.7 ms → 14 µs, no overflow past 170 terms).
21
+ - `IntegrationTechnique` evaluates the Gil–Pelaez integrals on a cached composite Gauss–Legendre rule
22
+ (`method="gauss"`, 0.69 → 0.07 ms; `method="quad"` keeps the adaptive rule); the calibration chain pricer
23
+ uses the same rule (400 options 19.5 → 1.7 ms). `FFTTechnique` interpolates cubically and defaults to
24
+ `eta=0.125` (ATM error 5e-4 → 1e-6 at the same cost).
25
+ - Longstaff–Schwartz: parallel power-sum regression on moneyness, time-major path matrices
26
+ (20k paths 32 → 13 ms); Crank–Nicolson PDE marches in numba with the two constant matrices factorised once
27
+ (200×200 grid 2.3 → 0.3 ms).
28
+ - Kernel signatures changed: SDE kernels take `(gen, n_paths, n_steps, antithetic, ...)`, path kernels return
29
+ `(n_steps + 1, n_paths)` arrays, and `longstaff_schwartz_pricer` takes `n_threads`.
30
+
9
31
  ### Changed
10
32
 
11
33
  - Python 3.10 support dropped (end of life); supported versions are 3.11–3.14, with 3.15 tested as experimental.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: optpricing
3
- Version: 2.5.0
3
+ Version: 2.6.0
4
4
  Summary: A financial options pricing and analysis library.
5
5
  Author: Diljit Singh
6
6
  License-Expression: MIT
@@ -83,6 +83,22 @@ def _lsmc_bsm_20k() -> float:
83
83
  return amc.price(PUT, STOCK, BSM, RATE).price
84
84
 
85
85
 
86
+ def _heston_chain_400() -> float:
87
+ import pandas as pd
88
+
89
+ from optpricing.calibration.vectorized_pricer import price_options_vectorized
90
+
91
+ chain = pd.DataFrame(
92
+ [
93
+ {"strike": k, "maturity": t, "optionType": typ}
94
+ for t in (0.1, 0.25, 0.5, 1.0, 2.0)
95
+ for k in np.linspace(60, 150, 40)
96
+ for typ in ("call", "put")
97
+ ]
98
+ )
99
+ return float(price_options_vectorized(chain, STOCK, HESTON, RATE, v0=0.04).sum())
100
+
101
+
86
102
  CASES: dict[str, tuple[Callable[[], float], str]] = {
87
103
  "bsm_closed_form_x1000": (_bsm_closed_form, "1000 closed-form BSM prices"),
88
104
  "heston_fft": (_heston_fft, "Carr-Madan FFT, N=4096"),
@@ -91,6 +107,10 @@ CASES: dict[str, tuple[Callable[[], float], str]] = {
91
107
  "mc_bsm_100k": (_mc_bsm_100k, "100k paths x 100 steps, antithetic"),
92
108
  "mc_heston_100k": (_mc_heston_100k, "100k paths x 100 steps, antithetic"),
93
109
  "lsmc_bsm_20k": (_lsmc_bsm_20k, "Longstaff-Schwartz, 20k paths x 50 steps"),
110
+ "heston_chain_400": (
111
+ _heston_chain_400,
112
+ "vectorised chain pricer, 400 options x 5 maturities",
113
+ ),
94
114
  }
95
115
 
96
116
 
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import numpy as np
4
4
  import pandas as pd
5
- from scipy.stats import norm
5
+ from scipy.special import ndtr
6
6
 
7
7
  from optpricing.atoms import Rate, Stock
8
8
 
@@ -78,13 +78,14 @@ class BSMIVSolver:
78
78
  sqrt_T = np.sqrt(T)
79
79
  d1 = (np.log(S / K) + (r - q + 0.5 * iv**2) * T) / (iv * sqrt_T)
80
80
  d2 = d1 - iv * sqrt_T
81
- vega = S * np.exp(-q * T) * sqrt_T * norm.pdf(d1)
82
- call_prices = S * np.exp(-q * T) * norm.cdf(d1) - K * np.exp(
83
- -r * T
84
- ) * norm.cdf(d2)
85
- put_prices = K * np.exp(-r * T) * norm.cdf(-d2) - S * np.exp(
86
- -q * T
87
- ) * norm.cdf(-d1)
81
+ pdf_d1 = np.exp(-0.5 * d1 * d1) / np.sqrt(2.0 * np.pi)
82
+ vega = S * np.exp(-q * T) * sqrt_T * pdf_d1
83
+ call_prices = S * np.exp(-q * T) * ndtr(d1) - K * np.exp(-r * T) * ndtr(
84
+ d2
85
+ )
86
+ put_prices = K * np.exp(-r * T) * ndtr(-d2) - S * np.exp(-q * T) * ndtr(
87
+ -d1
88
+ )
88
89
  model_prices = np.where(is_call, call_prices, put_prices)
89
90
  error = model_prices - target_prices
90
91
  if np.all(np.abs(error) < self.tolerance):
@@ -3,7 +3,8 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
5
  import numpy as np
6
- from scipy import integrate
6
+
7
+ from optpricing.techniques.integration import IntegrationTechnique
7
8
 
8
9
  if TYPE_CHECKING:
9
10
  import pandas as pd
@@ -64,15 +65,12 @@ def price_options_vectorized(
64
65
  phi = model.cf(t=T, spot=S, r=r, q=q, **kwargs)
65
66
  lnK = np.log(K)
66
67
 
67
- def _integrand_p2(u: float) -> np.ndarray:
68
- return (np.exp(-1j * u * lnK) * phi(u)).imag / u
69
-
70
- def _integrand_p1(u: float) -> np.ndarray:
71
- return (np.exp(-1j * u * lnK) * phi(u - 1j)).imag / u
72
-
73
- # Vectorised quad once per maturity
74
- p2, _ = integrate.quad_vec(_integrand_p2, 1e-15, upper_bound)
75
- p1, _ = integrate.quad_vec(_integrand_p1, 1e-15, upper_bound)
68
+ # Composite Gauss-Legendre (same rule as IntegrationTechnique): one CF
69
+ # evaluation per maturity, integrands as a (strikes x nodes) matrix.
70
+ u, w = IntegrationTechnique.gauss_nodes(upper_bound, 24)
71
+ twist = np.exp(-1j * np.outer(lnK, u)) / u # (n_strikes, n_nodes)
72
+ p2 = (twist * phi(u)).imag @ w
73
+ p1 = (twist * phi(u - 1j)).imag @ w
76
74
 
77
75
  denom = np.real(phi(-1j))
78
76
  denom = 1.0 if abs(denom) < 1e-12 else denom
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from collections.abc import Callable
4
4
 
5
5
  import numpy as np
6
- from scipy.stats import norm
6
+ from scipy.special import ndtr
7
7
 
8
8
  from optpricing.models.base import CF, BaseModel, ParamValidator, PDECoeffs
9
9
 
@@ -11,6 +11,13 @@ __doc__ = """
11
11
  Defines the Black-Scholes-Merton (BSM) model for pricing European options.
12
12
  """
13
13
 
14
+ _INV_SQRT_2PI = 0.3989422804014327
15
+
16
+
17
+ def _npdf(x):
18
+ """Standard normal density (scalar or array), without scipy.stats overhead."""
19
+ return _INV_SQRT_2PI * np.exp(-0.5 * x * x)
20
+
14
21
 
15
22
  class BSMModel(BaseModel):
16
23
  """
@@ -104,9 +111,9 @@ class BSMModel(BaseModel):
104
111
  df_rate = np.exp(-r * t)
105
112
 
106
113
  if call:
107
- price = spot * df_div * norm.cdf(d1) - strike * df_rate * norm.cdf(d2)
114
+ price = spot * df_div * ndtr(d1) - strike * df_rate * ndtr(d2)
108
115
  else:
109
- price = strike * df_rate * norm.cdf(-d2) - spot * df_div * norm.cdf(-d1)
116
+ price = strike * df_rate * ndtr(-d2) - spot * df_div * ndtr(-d1)
110
117
  return price
111
118
 
112
119
  def delta_analytic(
@@ -125,7 +132,7 @@ class BSMModel(BaseModel):
125
132
  sigma * np.sqrt(t)
126
133
  )
127
134
  df_div = np.exp(-q * t)
128
- return df_div * norm.cdf(d1) if call else -df_div * norm.cdf(-d1)
135
+ return df_div * ndtr(d1) if call else -df_div * ndtr(-d1)
129
136
 
130
137
  def gamma_analytic(
131
138
  self,
@@ -141,7 +148,7 @@ class BSMModel(BaseModel):
141
148
  sqrt_t = np.sqrt(t)
142
149
  d1 = (np.log(spot / strike) + (r - q + 0.5 * sigma**2) * t) / (sigma * sqrt_t)
143
150
  df_div = np.exp(-q * t)
144
- return df_div * norm.pdf(d1) / (spot * sigma * sqrt_t)
151
+ return df_div * _npdf(d1) / (spot * sigma * sqrt_t)
145
152
 
146
153
  def vega_analytic(
147
154
  self,
@@ -156,7 +163,7 @@ class BSMModel(BaseModel):
156
163
  sigma = self.params["sigma"]
157
164
  sqrt_t = np.sqrt(t)
158
165
  d1 = (np.log(spot / strike) + (r - q + 0.5 * sigma**2) * t) / (sigma * sqrt_t)
159
- return spot * np.exp(-q * t) * norm.pdf(d1) * sqrt_t
166
+ return spot * np.exp(-q * t) * _npdf(d1) * sqrt_t
160
167
 
161
168
  def theta_analytic(
162
169
  self,
@@ -173,13 +180,13 @@ class BSMModel(BaseModel):
173
180
  sqrt_t = np.sqrt(t)
174
181
  d1 = (np.log(spot / strike) + (r - q + 0.5 * sigma**2) * t) / (sigma * sqrt_t)
175
182
  d2 = d1 - sigma * sqrt_t
176
- term1 = -spot * np.exp(-q * t) * norm.pdf(d1) * sigma / (2 * sqrt_t)
183
+ term1 = -spot * np.exp(-q * t) * _npdf(d1) * sigma / (2 * sqrt_t)
177
184
  if call:
178
- term2 = q * spot * np.exp(-q * t) * norm.cdf(d1)
179
- term3 = -r * strike * np.exp(-r * t) * norm.cdf(d2)
185
+ term2 = q * spot * np.exp(-q * t) * ndtr(d1)
186
+ term3 = -r * strike * np.exp(-r * t) * ndtr(d2)
180
187
  else:
181
- term2 = -q * spot * np.exp(-q * t) * norm.cdf(-d1)
182
- term3 = r * strike * np.exp(-r * t) * norm.cdf(-d2)
188
+ term2 = -q * spot * np.exp(-q * t) * ndtr(-d1)
189
+ term3 = r * strike * np.exp(-r * t) * ndtr(-d2)
183
190
  return term1 + term2 + term3
184
191
 
185
192
  def rho_analytic(
@@ -197,7 +204,7 @@ class BSMModel(BaseModel):
197
204
  sqrt_t = np.sqrt(t)
198
205
  d2 = (np.log(spot / strike) + (r - q - 0.5 * sigma**2) * t) / (sigma * sqrt_t)
199
206
  df_rate = np.exp(-r * t)
200
- return strike * t * df_rate * (norm.cdf(d2) if call else -norm.cdf(-d2))
207
+ return strike * t * df_rate * (ndtr(d2) if call else -ndtr(-d2))
201
208
 
202
209
  def _cf_impl(
203
210
  self,
@@ -4,7 +4,7 @@ import math
4
4
  from typing import Any
5
5
 
6
6
  import numpy as np
7
- from scipy.special import factorial
7
+ from scipy.special import gammaln, ndtr
8
8
 
9
9
  from optpricing.models.base import CF, BaseModel, ParamValidator
10
10
  from optpricing.models.bsm import BSMModel
@@ -127,27 +127,39 @@ class MertonJumpModel(BaseModel):
127
127
  lambda_prime = lambda_ * (1 + k)
128
128
  y_mul = lambda_prime * t
129
129
 
130
- interval = np.arange(max_sum_terms)
131
- weights = np.exp(-y_mul) * (y_mul**interval) / factorial(interval)
132
-
133
- r_n = r - lambda_ * k + (interval * (mu_j + 0.5 * sigma_j**2)) / t
134
- sigma_n_sq = self.params["sigma"] ** 2 + (interval * sigma_j**2) / t
135
- sigma_n = np.sqrt(np.maximum(sigma_n_sq, 1e-12))
136
-
137
- total_price = 0.0
138
- for i in range(max_sum_terms):
139
- # Terminate sum early if weights become negligible
140
- if weights[i] < 1e-12 and i > lambda_prime * t:
141
- break
142
-
143
- temp_bsm_solver = self.bsm_solver.with_params(sigma=sigma_n[i])
144
-
145
- price_term = temp_bsm_solver.price_closed_form(
146
- spot=spot, strike=strike, r=r_n[i], q=q, t=t, call=call
147
- )
148
- total_price += weights[i] * price_term
130
+ n = np.arange(max_sum_terms)
131
+ # Poisson weights via logs (factorial overflows past n ~ 170).
132
+ log_weights = (
133
+ -y_mul + n * np.log(y_mul) - gammaln(n + 1.0) if y_mul > 0 else None
134
+ )
135
+ if log_weights is None:
136
+ weights = np.zeros(max_sum_terms)
137
+ weights[0] = 1.0
138
+ else:
139
+ weights = np.exp(log_weights)
140
+ # Truncate once the tail weight is negligible (past the mode).
141
+ keep = (weights >= 1e-12) | (n <= y_mul)
142
+ n_terms = int(np.max(np.nonzero(keep)[0])) + 1
143
+ n, weights = n[:n_terms], weights[:n_terms]
144
+
145
+ r_n = r - lambda_ * k + (n * (mu_j + 0.5 * sigma_j**2)) / t
146
+ sigma_n = np.sqrt(
147
+ np.maximum(self.params["sigma"] ** 2 + n * sigma_j**2 / t, 1e-12)
148
+ )
149
149
 
150
- return total_price
150
+ # Vectorised Black-Scholes-Merton over the series terms.
151
+ sqrt_t = math.sqrt(t)
152
+ d1 = (math.log(spot / strike) + (r_n - q + 0.5 * sigma_n**2) * t) / (
153
+ sigma_n * sqrt_t
154
+ )
155
+ d2 = d1 - sigma_n * sqrt_t
156
+ df_div = math.exp(-q * t)
157
+ df_rate = np.exp(-r_n * t)
158
+ if call:
159
+ prices = spot * df_div * ndtr(d1) - strike * df_rate * ndtr(d2)
160
+ else:
161
+ prices = strike * df_rate * ndtr(-d2) - spot * df_div * ndtr(-d1)
162
+ return float(np.dot(weights, prices))
151
163
 
152
164
  def _cf_impl(
153
165
  self,
@@ -5,7 +5,7 @@ import warnings
5
5
  from typing import Any
6
6
 
7
7
  import numpy as np
8
- from scipy.stats import norm
8
+ from scipy.special import ndtr
9
9
 
10
10
  from optpricing.models.base import BaseModel, ParamValidator
11
11
 
@@ -274,8 +274,8 @@ class SABRModel(BaseModel):
274
274
  d2 = d1 - sigma * sqrt_t
275
275
  discount = math.exp(-r * t)
276
276
  if call:
277
- return discount * (f_s * norm.cdf(d1) - k_s * norm.cdf(d2))
278
- return discount * (k_s * norm.cdf(-d2) - f_s * norm.cdf(-d1))
277
+ return discount * (f_s * ndtr(d1) - k_s * ndtr(d2))
278
+ return discount * (k_s * ndtr(-d2) - f_s * ndtr(-d1))
279
279
 
280
280
  # Abstract Method Implementations
281
281
  def _sde_impl(self, **kwargs: Any) -> Any:
@@ -4,17 +4,25 @@ import math
4
4
  from collections.abc import Callable
5
5
  from typing import Any
6
6
 
7
+ import numba
7
8
  import numpy as np
8
9
 
9
10
  from optpricing.atoms import Option, OptionType, Rate, Stock
10
11
  from optpricing.models import BaseModel, SABRJumpModel, SABRModel
11
12
  from optpricing.techniques.base import BaseTechnique, GreekMixin, IVMixin, PricingResult
13
+ from optpricing.techniques.base.random_utils import (
14
+ DEFAULT_CHUNK_SIZE,
15
+ plan_chunks,
16
+ run_chunks,
17
+ )
12
18
 
13
19
  from .kernels.american_mc_kernels import longstaff_schwartz_pricer
14
20
  from .kernels.path_kernels import (
15
21
  bates_path_kernel,
22
+ bates_qe_path_kernel,
16
23
  bsm_path_kernel,
17
24
  heston_path_kernel,
25
+ heston_qe_path_kernel,
18
26
  kou_path_kernel,
19
27
  merton_path_kernel,
20
28
  sabr_jump_path_kernel,
@@ -40,7 +48,23 @@ class AmericanMonteCarloTechnique(BaseTechnique, GreekMixin, IVMixin):
40
48
  antithetic: bool = True,
41
49
  seed: int | None = None,
42
50
  lsm_degree: int = 2,
51
+ n_jobs: int | None = None,
52
+ chunk_size: int = DEFAULT_CHUNK_SIZE,
53
+ variance_scheme: str = "qe",
43
54
  ):
55
+ """
56
+ Parameters
57
+ ----------
58
+ n_paths, n_steps, antithetic, seed, variance_scheme
59
+ As for `MonteCarloTechnique`.
60
+ lsm_degree : int, optional
61
+ Polynomial degree of the Longstaff-Schwartz regression, by default 2.
62
+ n_jobs : int | None, optional
63
+ Maximum number of threads simulating path chunks concurrently;
64
+ results do not depend on it.
65
+ chunk_size : int, optional
66
+ Paths per chunk and random stream, by default 8192.
67
+ """
44
68
  if antithetic and n_paths % 2 != 0:
45
69
  n_paths += 1
46
70
  self.n_paths = n_paths
@@ -48,6 +72,11 @@ class AmericanMonteCarloTechnique(BaseTechnique, GreekMixin, IVMixin):
48
72
  self.antithetic = antithetic
49
73
  self.rng = np.random.default_rng(seed)
50
74
  self.lsm_degree = lsm_degree
75
+ self.n_jobs = n_jobs
76
+ self.chunk_size = int(chunk_size)
77
+ if variance_scheme not in ("qe", "euler"):
78
+ raise ValueError("variance_scheme must be 'qe' or 'euler'.")
79
+ self.variance_scheme = variance_scheme
51
80
 
52
81
  def price(
53
82
  self,
@@ -69,6 +98,7 @@ class AmericanMonteCarloTechnique(BaseTechnique, GreekMixin, IVMixin):
69
98
  r=rate.get_rate(option.maturity),
70
99
  is_call=(option.option_type is OptionType.CALL),
71
100
  degree=self.lsm_degree,
101
+ n_threads=numba.get_num_threads(),
72
102
  )
73
103
  return PricingResult(price=price_val)
74
104
 
@@ -87,10 +117,10 @@ class AmericanMonteCarloTechnique(BaseTechnique, GreekMixin, IVMixin):
87
117
  if isinstance(model, SABRModel | SABRJumpModel):
88
118
  kernel_params.update(
89
119
  {
120
+ "v0": kwargs.get("v0", p["alpha"]),
90
121
  "nu": p["nu"],
91
122
  "beta": p["beta"],
92
123
  "rho": p["rho"],
93
- "v0": kwargs.get("v0", p["alpha"]),
94
124
  }
95
125
  )
96
126
  if model.has_jumps:
@@ -102,29 +132,37 @@ class AmericanMonteCarloTechnique(BaseTechnique, GreekMixin, IVMixin):
102
132
  if model.has_variance_process and model.has_jumps:
103
133
  kernel_params.update(
104
134
  {
135
+ "v0": kwargs.get("v0", p.get("v0")),
105
136
  "kappa": p["kappa"],
106
137
  "theta": p["theta"],
107
138
  "rho": p["rho"],
108
139
  "vol_of_vol": p["vol_of_vol"],
109
- "v0": kwargs.get("v0", p.get("v0")),
140
+ "lambda_": p["lambda"],
141
+ "mu_j": p["mu_j"],
142
+ "sigma_j": p["sigma_j"],
110
143
  }
111
144
  )
112
- kernel_params.update(
113
- {"lambda_": p["lambda"], "mu_j": p["mu_j"], "sigma_j": p["sigma_j"]}
114
- )
115
- return bates_path_kernel, kernel_params
116
- elif model.has_variance_process:
145
+ return (
146
+ bates_qe_path_kernel
147
+ if self.variance_scheme == "qe"
148
+ else bates_path_kernel
149
+ ), kernel_params
150
+ if model.has_variance_process:
117
151
  kernel_params.update(
118
152
  {
153
+ "v0": kwargs.get("v0", p.get("v0")),
119
154
  "kappa": p["kappa"],
120
155
  "theta": p["theta"],
121
156
  "rho": p["rho"],
122
157
  "vol_of_vol": p["vol_of_vol"],
123
- "v0": kwargs.get("v0", p.get("v0")),
124
158
  }
125
159
  )
126
- return heston_path_kernel, kernel_params
127
- elif model.name == "Kou Double-Exponential Jump":
160
+ return (
161
+ heston_qe_path_kernel
162
+ if self.variance_scheme == "qe"
163
+ else heston_path_kernel
164
+ ), kernel_params
165
+ if model.name == "Kou Double-Exponential Jump":
128
166
  kernel_params.update(
129
167
  {
130
168
  "sigma": p["sigma"],
@@ -135,7 +173,7 @@ class AmericanMonteCarloTechnique(BaseTechnique, GreekMixin, IVMixin):
135
173
  }
136
174
  )
137
175
  return kou_path_kernel, kernel_params
138
- elif model.has_jumps:
176
+ if model.has_jumps:
139
177
  kernel_params.update(
140
178
  {
141
179
  "sigma": p["sigma"],
@@ -145,9 +183,8 @@ class AmericanMonteCarloTechnique(BaseTechnique, GreekMixin, IVMixin):
145
183
  }
146
184
  )
147
185
  return merton_path_kernel, kernel_params
148
- else:
149
- kernel_params["sigma"] = p.get("sigma")
150
- return bsm_path_kernel, kernel_params
186
+ kernel_params["sigma"] = p["sigma"]
187
+ return bsm_path_kernel, kernel_params
151
188
 
152
189
  def _simulate_full_sde_paths(
153
190
  self,
@@ -157,61 +194,46 @@ class AmericanMonteCarloTechnique(BaseTechnique, GreekMixin, IVMixin):
157
194
  rate: Rate,
158
195
  **kwargs: Any,
159
196
  ) -> np.ndarray:
160
- """Simulates and returns the full matrix of stock price paths."""
197
+ """Simulates the spot paths, time-major ``(n_steps + 1, n_paths)``."""
161
198
  S0, T = stock.spot, option.maturity
162
199
  r, q = rate.get_rate(T), stock.dividend
163
200
  dt = T / self.n_steps
164
- num_draws = self.n_paths // 2 if self.antithetic else self.n_paths
165
201
 
166
202
  kernel, kernel_params = self._get_path_kernel_and_params(
167
203
  model, r, q, dt, **kwargs
168
204
  )
169
-
170
- sim_params = {
171
- "n_paths": num_draws,
172
- "n_steps": self.n_steps,
173
- **kernel_params,
174
- }
175
205
  is_sabr = isinstance(model, SABRModel | SABRJumpModel)
176
206
  if is_sabr:
177
207
  # SABR evolves the (shifted) forward; the path kernels convert the
178
208
  # forward paths back to spot paths for the exercise decision.
179
- sim_params["s0"] = S0 * math.exp((r - q) * T) + model.shift
209
+ kernel_params["f0"] = S0 * math.exp((r - q) * T) + model.shift
180
210
  else:
181
- sim_params["log_s0"] = math.log(S0)
182
-
183
- # Generate standard normal draws (z) first, then scale by sqrt(dt)
184
- if model.has_variance_process:
185
- z1 = self.rng.standard_normal(size=(num_draws, self.n_steps))
186
- z2 = self.rng.standard_normal(size=(num_draws, self.n_steps))
187
- sim_params["dw1"] = z1 * math.sqrt(dt)
188
- sim_params["dw2"] = z2 * math.sqrt(dt)
189
- else:
190
- z = self.rng.standard_normal(size=(num_draws, self.n_steps))
191
- sim_params["dw"] = z * math.sqrt(dt)
192
-
193
- if model.has_jumps:
194
- sim_params["jump_counts"] = self.rng.poisson(
195
- lam=model.params["lambda"] * dt, size=(num_draws, self.n_steps)
196
- )
197
-
198
- paths = kernel(**sim_params)
211
+ kernel_params["log_s0"] = math.log(S0)
199
212
 
200
- if self.antithetic:
201
- if "dw" in sim_params:
202
- sim_params["dw"] *= -1
203
- if "dw1" in sim_params:
204
- sim_params["dw1"] *= -1
205
- sim_params["dw2"] *= -1
206
-
207
- paths_anti = kernel(**sim_params)
208
- paths = np.concatenate([paths, paths_anti])
213
+ generators, sizes = plan_chunks(
214
+ self.rng,
215
+ self.n_paths,
216
+ chunk_size=self.chunk_size,
217
+ antithetic=self.antithetic,
218
+ )
219
+ paths = run_chunks(
220
+ kernel,
221
+ generators,
222
+ sizes,
223
+ n_jobs=self.n_jobs,
224
+ axis=1,
225
+ n_steps=self.n_steps,
226
+ antithetic=self.antithetic,
227
+ **kernel_params,
228
+ )
209
229
 
210
230
  if is_sabr and model.shift:
211
231
  # The kernels discount the shifted forward; remove the shift so the
212
232
  # exercise decision sees spot prices.
213
233
  horizon = self.n_steps * dt
214
234
  times = np.arange(self.n_steps + 1) * dt
215
- paths = paths - model.shift * np.exp(-(r - q) * (horizon - times))
235
+ paths = (
236
+ paths - (model.shift * np.exp(-(r - q) * (horizon - times)))[:, None]
237
+ )
216
238
 
217
239
  return paths