optPricing 2.6.0__tar.gz → 2.7.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 (236) hide show
  1. {optpricing-2.6.0 → optpricing-2.7.0}/CHANGELOG.md +61 -0
  2. {optpricing-2.6.0/src/optpricing.egg-info → optpricing-2.7.0}/PKG-INFO +3 -2
  3. {optpricing-2.6.0 → optpricing-2.7.0}/pyproject.toml +3 -1
  4. optpricing-2.7.0/src/optpricing/calibration/__init__.py +54 -0
  5. optpricing-2.7.0/src/optpricing/calibration/calibrator.py +480 -0
  6. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/calibration/fit_jump_parameters.py +11 -7
  7. optpricing-2.7.0/src/optpricing/calibration/fit_market_params.py +271 -0
  8. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/calibration/iv_surface.py +17 -5
  9. optpricing-2.7.0/src/optpricing/calibration/market_filters.py +307 -0
  10. optpricing-2.7.0/src/optpricing/calibration/metrics.py +181 -0
  11. optpricing-2.7.0/src/optpricing/calibration/vectorized_bsm_iv.py +170 -0
  12. optpricing-2.7.0/src/optpricing/calibration/vectorized_pricer.py +313 -0
  13. optpricing-2.7.0/src/optpricing/cli/commands/backtest.py +133 -0
  14. optpricing-2.7.0/src/optpricing/cli/commands/calibrate.py +265 -0
  15. optpricing-2.7.0/src/optpricing/cli/commands/data.py +243 -0
  16. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/cli/commands/price.py +10 -16
  17. optpricing-2.7.0/src/optpricing/cli/commands/tune.py +129 -0
  18. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/cli/main.py +8 -2
  19. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/config.py +4 -0
  20. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/dashboard/pages/2_Calibration_and_IV.py +63 -12
  21. optpricing-2.7.0/src/optpricing/dashboard/service.py +192 -0
  22. optpricing-2.7.0/src/optpricing/data/__init__.py +53 -0
  23. optpricing-2.7.0/src/optpricing/data/historical_manager.py +301 -0
  24. optpricing-2.7.0/src/optpricing/data/market_data_manager.py +636 -0
  25. optpricing-2.7.0/src/optpricing/data/stream.py +314 -0
  26. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/base/base_model.py +4 -0
  27. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/bates.py +7 -1
  28. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/bsm.py +15 -9
  29. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/cev.py +121 -4
  30. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/merton_jump.py +28 -15
  31. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/sabr.py +24 -14
  32. optpricing-2.7.0/src/optpricing/workflows/__init__.py +29 -0
  33. optpricing-2.7.0/src/optpricing/workflows/backtest_workflow.py +299 -0
  34. optpricing-2.7.0/src/optpricing/workflows/configs/__init__.py +127 -0
  35. optpricing-2.7.0/src/optpricing/workflows/configs/bsm_config.py +13 -0
  36. optpricing-2.7.0/src/optpricing/workflows/configs/cev_config.py +23 -0
  37. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/workflows/configs/cgmy_config.py +2 -2
  38. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/workflows/configs/heston_config.py +7 -3
  39. optpricing-2.7.0/src/optpricing/workflows/configs/kou_config.py +29 -0
  40. optpricing-2.7.0/src/optpricing/workflows/configs/merton_config.py +29 -0
  41. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/workflows/configs/sabr_jump_config.py +4 -2
  42. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/workflows/configs/vg_config.py +1 -1
  43. optpricing-2.7.0/src/optpricing/workflows/daily_workflow.py +330 -0
  44. optpricing-2.7.0/src/optpricing/workflows/tuning.py +269 -0
  45. {optpricing-2.6.0 → optpricing-2.7.0/src/optpricing.egg-info}/PKG-INFO +3 -2
  46. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing.egg-info/SOURCES.txt +15 -1
  47. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing.egg-info/requires.txt +2 -1
  48. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing.egg-info/scm_file_list.json +26 -0
  49. optpricing-2.7.0/src/optpricing.egg-info/scm_version.json +8 -0
  50. optpricing-2.7.0/tests/calibration/test_calibrator.py +191 -0
  51. optpricing-2.7.0/tests/calibration/test_fit_market_params.py +134 -0
  52. optpricing-2.7.0/tests/calibration/test_market_filters.py +132 -0
  53. optpricing-2.7.0/tests/calibration/test_metrics.py +68 -0
  54. optpricing-2.7.0/tests/calibration/test_vectorized_bsm_iv.py +116 -0
  55. optpricing-2.7.0/tests/calibration/test_vectorized_pricer.py +186 -0
  56. optpricing-2.7.0/tests/cli/commands/test_backtest.py +57 -0
  57. optpricing-2.7.0/tests/cli/commands/test_calibrate.py +120 -0
  58. optpricing-2.7.0/tests/cli/commands/test_data.py +129 -0
  59. optpricing-2.7.0/tests/cli/commands/test_price.py +103 -0
  60. optpricing-2.7.0/tests/cli/commands/test_tune.py +49 -0
  61. optpricing-2.7.0/tests/conftest.py +101 -0
  62. optpricing-2.7.0/tests/dashboard/test_pages.py +78 -0
  63. optpricing-2.7.0/tests/dashboard/test_service.py +166 -0
  64. optpricing-2.7.0/tests/data/test_historical_manager.py +120 -0
  65. optpricing-2.7.0/tests/data/test_market_data_manager.py +368 -0
  66. optpricing-2.7.0/tests/data/test_stream.py +155 -0
  67. optpricing-2.7.0/tests/models/test_vectorized_closed_forms.py +101 -0
  68. optpricing-2.7.0/tests/workflows/test_backtest_workflow.py +125 -0
  69. optpricing-2.7.0/tests/workflows/test_configs.py +74 -0
  70. optpricing-2.7.0/tests/workflows/test_daily_workflow.py +179 -0
  71. optpricing-2.7.0/tests/workflows/test_tuning.py +104 -0
  72. optpricing-2.6.0/src/optpricing/calibration/__init__.py +0 -28
  73. optpricing-2.6.0/src/optpricing/calibration/calibrator.py +0 -149
  74. optpricing-2.6.0/src/optpricing/calibration/fit_market_params.py +0 -120
  75. optpricing-2.6.0/src/optpricing/calibration/vectorized_bsm_iv.py +0 -94
  76. optpricing-2.6.0/src/optpricing/calibration/vectorized_pricer.py +0 -86
  77. optpricing-2.6.0/src/optpricing/cli/commands/backtest.py +0 -46
  78. optpricing-2.6.0/src/optpricing/cli/commands/calibrate.py +0 -145
  79. optpricing-2.6.0/src/optpricing/cli/commands/data.py +0 -165
  80. optpricing-2.6.0/src/optpricing/dashboard/service.py +0 -103
  81. optpricing-2.6.0/src/optpricing/data/__init__.py +0 -28
  82. optpricing-2.6.0/src/optpricing/data/historical_manager.py +0 -91
  83. optpricing-2.6.0/src/optpricing/data/market_data_manager.py +0 -200
  84. optpricing-2.6.0/src/optpricing/workflows/__init__.py +0 -14
  85. optpricing-2.6.0/src/optpricing/workflows/backtest_workflow.py +0 -128
  86. optpricing-2.6.0/src/optpricing/workflows/configs/__init__.py +0 -53
  87. optpricing-2.6.0/src/optpricing/workflows/configs/bsm_config.py +0 -10
  88. optpricing-2.6.0/src/optpricing/workflows/configs/cev_config.py +0 -16
  89. optpricing-2.6.0/src/optpricing/workflows/configs/kou_config.py +0 -20
  90. optpricing-2.6.0/src/optpricing/workflows/configs/merton_config.py +0 -12
  91. optpricing-2.6.0/src/optpricing/workflows/daily_workflow.py +0 -217
  92. optpricing-2.6.0/src/optpricing.egg-info/scm_version.json +0 -8
  93. optpricing-2.6.0/tests/calibration/test_calibrator.py +0 -99
  94. optpricing-2.6.0/tests/calibration/test_fit_market_params.py +0 -79
  95. optpricing-2.6.0/tests/calibration/test_vectorized_bsm_iv.py +0 -47
  96. optpricing-2.6.0/tests/calibration/test_vectorized_pricer.py +0 -75
  97. optpricing-2.6.0/tests/cli/commands/test_backtest.py +0 -20
  98. optpricing-2.6.0/tests/cli/commands/test_calibrate.py +0 -39
  99. optpricing-2.6.0/tests/cli/commands/test_data.py +0 -53
  100. optpricing-2.6.0/tests/cli/commands/test_price.py +0 -60
  101. optpricing-2.6.0/tests/dashboard/test_service.py +0 -114
  102. optpricing-2.6.0/tests/data/test_historical_manager.py +0 -58
  103. optpricing-2.6.0/tests/data/test_market_data_manager.py +0 -193
  104. optpricing-2.6.0/tests/workflows/test_backtest_workflow.py +0 -73
  105. optpricing-2.6.0/tests/workflows/test_daily_workflow.py +0 -76
  106. {optpricing-2.6.0 → optpricing-2.7.0}/LICENSE +0 -0
  107. {optpricing-2.6.0 → optpricing-2.7.0}/MANIFEST.in +0 -0
  108. {optpricing-2.6.0 → optpricing-2.7.0}/README.md +0 -0
  109. {optpricing-2.6.0 → optpricing-2.7.0}/benchmarks/run_benchmarks.py +0 -0
  110. {optpricing-2.6.0 → optpricing-2.7.0}/setup.cfg +0 -0
  111. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/__init__.py +0 -0
  112. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/atoms/__init__.py +0 -0
  113. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/atoms/bond.py +0 -0
  114. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/atoms/option.py +0 -0
  115. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/atoms/rate.py +0 -0
  116. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/atoms/stock.py +0 -0
  117. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/calibration/technique_selector.py +0 -0
  118. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/calibration/vectorized_integration_iv.py +0 -0
  119. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/cli/__init__.py +0 -0
  120. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/cli/commands/__init__.py +0 -0
  121. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/cli/commands/dashboard.py +0 -0
  122. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/cli/commands/demo.py +0 -0
  123. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/cli/commands/tools.py +0 -0
  124. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/dashboard/Home.py +0 -0
  125. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/dashboard/__init__.py +0 -0
  126. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/dashboard/pages/1_Pricer_and_Greeks.py +0 -0
  127. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/dashboard/pages/3_Market_Analytics.py +0 -0
  128. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/dashboard/pages/4_Model_Fitting.py +0 -0
  129. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/dashboard/pages/5_Term_Structure.py +0 -0
  130. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/dashboard/pages/__init__.py +0 -0
  131. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/dashboard/plots.py +0 -0
  132. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/dashboard/widgets.py +0 -0
  133. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/__init__.py +0 -0
  134. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/base/__init__.py +0 -0
  135. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/base/cf_sampler.py +0 -0
  136. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/base/validators.py +0 -0
  137. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/blacks_approx.py +0 -0
  138. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/cgmy.py +0 -0
  139. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/cir.py +0 -0
  140. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/dupire_local.py +0 -0
  141. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/heston.py +0 -0
  142. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/hyperbolic.py +0 -0
  143. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/kou.py +0 -0
  144. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/nig.py +0 -0
  145. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/perpetual_put.py +0 -0
  146. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/sabr_jump.py +0 -0
  147. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/vasicek.py +0 -0
  148. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/vg.py +0 -0
  149. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/parity/__init__.py +0 -0
  150. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/parity/implied_rate.py +0 -0
  151. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/parity/parity_model.py +0 -0
  152. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/__init__.py +0 -0
  153. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/american_monte_carlo.py +0 -0
  154. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/base/__init__.py +0 -0
  155. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/base/base_technique.py +0 -0
  156. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/base/greek_mixin.py +0 -0
  157. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/base/iv_mixin.py +0 -0
  158. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/base/lattice_technique.py +0 -0
  159. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/base/pricing_result.py +0 -0
  160. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/base/random_utils.py +0 -0
  161. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/closed_form.py +0 -0
  162. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/crr.py +0 -0
  163. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/fft.py +0 -0
  164. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/integration.py +0 -0
  165. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/kernels/__init__.py +0 -0
  166. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/kernels/american_mc_kernels.py +0 -0
  167. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/kernels/lattice_kernels.py +0 -0
  168. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/kernels/mc_kernels.py +0 -0
  169. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/kernels/path_kernels.py +0 -0
  170. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/leisen_reimer.py +0 -0
  171. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/monte_carlo.py +0 -0
  172. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/pde.py +0 -0
  173. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/topm.py +0 -0
  174. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/workflows/configs/bates_config.py +0 -0
  175. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/workflows/configs/hyperbolic_config.py +0 -0
  176. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/workflows/configs/nig_config.py +0 -0
  177. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/workflows/configs/sabr_config.py +0 -0
  178. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing.egg-info/dependency_links.txt +0 -0
  179. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing.egg-info/entry_points.txt +0 -0
  180. {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing.egg-info/top_level.txt +0 -0
  181. {optpricing-2.6.0 → optpricing-2.7.0}/tests/atoms/test_bond.py +0 -0
  182. {optpricing-2.6.0 → optpricing-2.7.0}/tests/atoms/test_option.py +0 -0
  183. {optpricing-2.6.0 → optpricing-2.7.0}/tests/atoms/test_rate.py +0 -0
  184. {optpricing-2.6.0 → optpricing-2.7.0}/tests/atoms/test_stock.py +0 -0
  185. {optpricing-2.6.0 → optpricing-2.7.0}/tests/calibration/test_fit_jump_parameters.py +0 -0
  186. {optpricing-2.6.0 → optpricing-2.7.0}/tests/calibration/test_iv_surface.py +0 -0
  187. {optpricing-2.6.0 → optpricing-2.7.0}/tests/calibration/test_technique_selector.py +0 -0
  188. {optpricing-2.6.0 → optpricing-2.7.0}/tests/calibration/test_vectorized_integration_iv.py +0 -0
  189. {optpricing-2.6.0 → optpricing-2.7.0}/tests/cli/commands/test_tools.py +0 -0
  190. {optpricing-2.6.0 → optpricing-2.7.0}/tests/cli/test_main.py +0 -0
  191. {optpricing-2.6.0 → optpricing-2.7.0}/tests/correctness/__init__.py +0 -0
  192. {optpricing-2.6.0 → optpricing-2.7.0}/tests/correctness/_helpers.py +0 -0
  193. {optpricing-2.6.0 → optpricing-2.7.0}/tests/correctness/conftest.py +0 -0
  194. {optpricing-2.6.0 → optpricing-2.7.0}/tests/correctness/test_invariants.py +0 -0
  195. {optpricing-2.6.0 → optpricing-2.7.0}/tests/correctness/test_mc_regressions.py +0 -0
  196. {optpricing-2.6.0 → optpricing-2.7.0}/tests/correctness/test_reference_values.py +0 -0
  197. {optpricing-2.6.0 → optpricing-2.7.0}/tests/dashboard/test_plots.py +0 -0
  198. {optpricing-2.6.0 → optpricing-2.7.0}/tests/dashboard/test_widgets.py +0 -0
  199. {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/base/test_base_model.py +0 -0
  200. {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/base/test_validators.py +0 -0
  201. {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_bates.py +0 -0
  202. {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_blacks_approx.py +0 -0
  203. {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_bsm.py +0 -0
  204. {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_cev.py +0 -0
  205. {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_cgmy.py +0 -0
  206. {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_cir.py +0 -0
  207. {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_dupire_local.py +0 -0
  208. {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_heston.py +0 -0
  209. {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_hyperbolic.py +0 -0
  210. {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_kou.py +0 -0
  211. {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_merton_jump.py +0 -0
  212. {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_nig.py +0 -0
  213. {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_perpetual_put.py +0 -0
  214. {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_sabr.py +0 -0
  215. {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_sabr_jump.py +0 -0
  216. {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_vasicek.py +0 -0
  217. {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_vg.py +0 -0
  218. {optpricing-2.6.0 → optpricing-2.7.0}/tests/parity/test_implied_rate.py +0 -0
  219. {optpricing-2.6.0 → optpricing-2.7.0}/tests/parity/test_parity_model.py +0 -0
  220. {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/base/test_greek_mixin.py +0 -0
  221. {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/base/test_iv_mixin.py +0 -0
  222. {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/base/test_pricing_result.py +0 -0
  223. {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/base/test_random_utils.py +0 -0
  224. {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/kernels/test_lattice_kernels.py +0 -0
  225. {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/kernels/test_mc_kernels.py +0 -0
  226. {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/kernels/test_path_kernels.py +0 -0
  227. {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/test_american_monte_carlo.py +0 -0
  228. {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/test_closed_form.py +0 -0
  229. {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/test_crr.py +0 -0
  230. {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/test_fft.py +0 -0
  231. {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/test_integration.py +0 -0
  232. {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/test_leisen_reimer.py +0 -0
  233. {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/test_monte_carlo.py +0 -0
  234. {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/test_pde.py +0 -0
  235. {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/test_topm.py +0 -0
  236. {optpricing-2.6.0 → optpricing-2.7.0}/tests/test_config.py +0 -0
@@ -6,6 +6,67 @@ All notable changes to this project are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ### Workflows (calibration, backtesting, data)
10
+
11
+ - **Calibration rebuilt** (`Calibrator.fit` keeps its signature). Residuals are taken in Black-Scholes
12
+ implied-volatility space (`objective="iv"`, price space optional) with inverse bid-ask-spread weights
13
+ (`"vega"`/`"none"` optional); parameters are optimised in a unit cube (`log_scale` for wide ranges) with
14
+ L-BFGS-B from several starting points (recipe guess, warm start from the previous day, Latin-hypercube
15
+ samples); invalid parameter sets are penalised instead of raising; optional soft Feller penalty; `logging`
16
+ replaces the per-evaluation prints; `Calibrator.result` (`CalibrationResult`) carries loss, IV/price RMSE,
17
+ evaluation counts and per-start records.
18
+ - New `LiquidityFilter` / `filter_options` (OTM options with a bid, spread ≤ 25 %, open interest, 7 days–2 years,
19
+ forward moneyness 0.7–1.3, solvable implied vol) and `CalibrationSettings`; fit metrics (`evaluate_model`,
20
+ `residual_table`, `bucket_metrics`) by maturity and moneyness bucket.
21
+ - **Every model with a closed form or characteristic function is calibratable** (BSM, Merton, Heston, Bates,
22
+ SABR, CEV, Kou, NIG, VG, CGMY, Hyperbolic); SABR-with-jumps via Monte Carlo behind `--allow-mc`.
23
+ `price_options_vectorized` dispatches to vectorised closed forms (BSM, Merton, SABR, **CEV**), the
24
+ Gauss-Legendre characteristic-function route or Monte Carlo with common random numbers; `ChainLayout` caches
25
+ the strike groups and Fourier kernels for repeated pricing (Heston objective 13 → 4 ms on a 2,700-option chain).
26
+ - `CEVModel.price_closed_form`: Cox (1975)/Schroder (1989) noncentral-χ² formula, matching the Hull reference
27
+ values and QuantLib's `AnalyticCEVEngine` to 1e-9 (CEV previously had no closed form).
28
+ - `BSMIVSolver`: safeguarded Newton with bracket tracking and bisection fallback, NaN for prices outside the
29
+ no-arbitrage band (deep ITM/OTM and short maturities used to return garbage), optional initial guess.
30
+ - **Snapshot schema v2**: `ticker`, `snapshot_date`, `as_of`, `risk_free_rate` (13-week T-bill `^IRX`,
31
+ continuously compounded) and `dividend_yield` (implied from put-call parity near the money, given the rate)
32
+ are stored with every chain; v1 files are upgraded on load from the stored `^IRX` history
33
+ (`optpricing data download --rates`). Workflows no longer call the network.
34
+ - Historical files (v2) store adjusted close, dividends and log returns; `trailing_dividend_yield`,
35
+ `load_risk_free_rate`, `get_live_risk_free_rate` added.
36
+ - `BacktestWorkflow` is a proper walk-forward test: ascending dates, calibrate on *t*, evaluate on *t+1* with
37
+ that day's market parameters, out-of-sample IV/price RMSE, bucket metrics, `BSM-ATM` and stale-parameter
38
+ baselines, warm starts; CSV + bucket CSV + JSON summary.
39
+ - New per-ticker auto-tuning (`optpricing tune`, `workflows.tune_ticker`): walk-forward cross-validation over
40
+ candidate settings, model leaderboard, cache in `artifacts/tuning/{TICKER}.json` used by `calibrate --auto`,
41
+ `backtest --auto` and the dashboard.
42
+ - CLI: `calibrate --model all|<names> [--auto] [--allow-mc] [--objective] [--n-starts]` with a leaderboard
43
+ table and CSV, `backtest --model all`, new `tune`, `data download --rates`, `data snapshot --compact
44
+ --only-fresh`, new `data stream` (free `yfinance.WebSocket` quotes → parquet, option contracts included).
45
+ Model names are case-insensitive with aliases (`Variance Gamma` → `VG`).
46
+ - Scheduled GitHub Action (`snapshot.yml`) captures compact liquid-only chains for `snapshot_tickers` every
47
+ trading day (~25 KB per ticker) and commits them.
48
+ - Dashboard: calibration page offers every calibratable model, tuned settings, objective/start controls, a
49
+ leaderboard sorted by IV RMSE and error-by-bucket tables; `DashboardService` runs on the offline workflow.
50
+ Headless `AppTest` smoke tests cover every page.
51
+
52
+ ### Fixed (workflows)
53
+
54
+ - Backtest paired dates newest-first, calibrating on *t+1* and evaluating on *t* (look-ahead bias).
55
+ - Calibration workflows fetched today's dividend yield from yfinance for historical snapshots (and failed offline).
56
+ - Recipes were ignored when a model had `param_defs`; Bates fitted a non-existent `sigma`; Kou's `frozen` was a
57
+ list and crashed; Merton froze all four parameters from history and fitted nothing; Heston started from a
58
+ Feller-violating guess; the VG recipe key (`"Variance Gamma"`) did not match the dashboard's `"VG"`.
59
+ - Implied dividend yields: inferring both `r` and `q` from the strike slope of `C − P` is biased by the early
60
+ exercise premium of American puts (real SPY chains implied r ≈ 0 and q ≈ −3 %); the rate now comes from the
61
+ T-bill quote and only near-the-money pairs are used for `q`.
62
+ - yfinance 1.x: `fast_info["last_price"]` → `lastPrice` (every live fetch triggered a second `history()` call);
63
+ `dividendYield` is reported in percent since 0.2.5x (unit detection added); duplicate contracts dropped.
64
+ - Heston characteristic function at `u = −i` is degenerate when ρξ > κ; the chain pricer falls back to the
65
+ analytic forward there and silences the resulting warnings during calibration.
66
+ - `calibrate` printed the parameters at every objective evaluation.
67
+ - `pyarrow` (the parquet engine every snapshot needs) was only installed through the `ui` extra; it is now a
68
+ runtime dependency (`pyarrow>=22`, the first release with Python 3.14 wheels).
69
+
9
70
  ### Performance
10
71
 
11
72
  - Monte Carlo engine rebuilt: the numba kernels draw their own random numbers from per-chunk
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: optpricing
3
- Version: 2.6.0
3
+ Version: 2.7.0
4
4
  Summary: A financial options pricing and analysis library.
5
5
  Author: Diljit Singh
6
6
  License-Expression: MIT
@@ -30,8 +30,9 @@ Requires-Dist: scipy>=1.16.1
30
30
  Requires-Dist: pandas>=2.3.3
31
31
  Requires-Dist: numba>=0.63
32
32
  Requires-Dist: pyyaml>=6.0.3
33
- Requires-Dist: typer>=0.15.3
33
+ Requires-Dist: typer>=0.16.0
34
34
  Requires-Dist: yfinance>=0.2.63
35
+ Requires-Dist: pyarrow>=22
35
36
  Provides-Extra: ui
36
37
  Requires-Dist: streamlit>=1.45.1; extra == "ui"
37
38
  Requires-Dist: plotly>=6.1.2; extra == "ui"
@@ -45,8 +45,10 @@ dependencies = [
45
45
  "pandas>=2.3.3",
46
46
  "numba>=0.63",
47
47
  "pyyaml>=6.0.3",
48
- "typer>=0.15.3",
48
+ "typer>=0.16.0",
49
49
  "yfinance>=0.2.63",
50
+ # Parquet engine for snapshots and recordings; 22.0.0 is the first release with cp314 wheels.
51
+ "pyarrow>=22",
50
52
  ]
51
53
 
52
54
  [project.optional-dependencies]
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ __doc__ = """
4
+ The `calibration` package provides tools for fitting financial models to
5
+ market data, estimating parameters from historical data, and calculating
6
+ implied volatility surfaces.
7
+ """
8
+
9
+ from .calibrator import CalibrationResult, Calibrator
10
+ from .fit_jump_parameters import fit_jump_params_from_history
11
+ from .fit_market_params import (
12
+ find_atm_options,
13
+ fit_rate_and_dividend,
14
+ implied_dividend_yield,
15
+ implied_forward_curve,
16
+ )
17
+ from .iv_surface import VolatilitySurface
18
+ from .market_filters import (
19
+ DEFAULT_SETTINGS,
20
+ CalibrationSettings,
21
+ LiquidityFilter,
22
+ annotate_chain,
23
+ filter_options,
24
+ )
25
+ from .metrics import bucket_metrics, evaluate_model, residual_table, summarize_residuals
26
+ from .technique_selector import select_fastest_technique
27
+ from .vectorized_bsm_iv import BSMIVSolver
28
+ from .vectorized_integration_iv import VectorizedIntegrationIVSolver
29
+ from .vectorized_pricer import price_options_vectorized, supports_vectorized_pricing
30
+
31
+ __all__ = [
32
+ "BSMIVSolver",
33
+ "CalibrationResult",
34
+ "CalibrationSettings",
35
+ "Calibrator",
36
+ "DEFAULT_SETTINGS",
37
+ "LiquidityFilter",
38
+ "VectorizedIntegrationIVSolver",
39
+ "VolatilitySurface",
40
+ "annotate_chain",
41
+ "bucket_metrics",
42
+ "evaluate_model",
43
+ "filter_options",
44
+ "fit_jump_params_from_history",
45
+ "fit_rate_and_dividend",
46
+ "find_atm_options",
47
+ "implied_dividend_yield",
48
+ "implied_forward_curve",
49
+ "price_options_vectorized",
50
+ "residual_table",
51
+ "select_fastest_technique",
52
+ "summarize_residuals",
53
+ "supports_vectorized_pricing",
54
+ ]
@@ -0,0 +1,480 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import time
5
+ from collections.abc import Iterable
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+ from scipy.optimize import minimize
12
+ from scipy.stats import qmc
13
+
14
+ from optpricing.atoms import Rate, Stock
15
+ from optpricing.models import BaseModel
16
+
17
+ from .market_filters import annotate_chain
18
+ from .technique_selector import select_fastest_technique
19
+ from .vectorized_bsm_iv import BSMIVSolver, bsm_prices_and_vegas
20
+ from .vectorized_pricer import ChainLayout, price_chain, supports_vectorized_pricing
21
+
22
+ __doc__ = """
23
+ Defines the main Calibrator class used to fit financial models to market data.
24
+ """
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ _FELLER_KEYS = ("kappa", "theta", "vol_of_vol")
29
+
30
+
31
+ @dataclass
32
+ class CalibrationResult:
33
+ """
34
+ Outcome of :meth:`Calibrator.fit`.
35
+
36
+ Attributes
37
+ ----------
38
+ params : dict[str, float]
39
+ Calibrated (and frozen) parameters.
40
+ loss : float
41
+ Final weighted mean squared residual (IV² or price² units).
42
+ rmse_iv : float
43
+ Unweighted RMSE of the implied-volatility residuals at the optimum
44
+ (NaN when no option admits an implied volatility).
45
+ rmse_price : float
46
+ Unweighted RMSE of the price residuals at the optimum.
47
+ n_options : int
48
+ Size of the calibration set.
49
+ n_evaluations : int
50
+ Objective evaluations (all starts).
51
+ n_starts : int
52
+ Starting points tried.
53
+ runtime : float
54
+ Wall-clock seconds.
55
+ success : bool
56
+ Whether at least one local optimisation converged.
57
+ message : str
58
+ Optimiser message of the best run.
59
+ starts : list[dict[str, Any]]
60
+ Per-start records (``params``, ``loss``, ``kind``).
61
+ """
62
+
63
+ params: dict[str, float]
64
+ loss: float
65
+ rmse_iv: float
66
+ rmse_price: float
67
+ n_options: int
68
+ n_evaluations: int
69
+ n_starts: int
70
+ runtime: float
71
+ success: bool
72
+ message: str = ""
73
+ starts: list[dict[str, Any]] = field(default_factory=list)
74
+
75
+
76
+ class Calibrator:
77
+ """
78
+ Fits a model's parameters to an option chain.
79
+
80
+ The residuals are taken in Black-Scholes implied-volatility space by
81
+ default (``objective="iv"``), which puts every strike and maturity on the
82
+ same footing; ``objective="price"`` uses price differences. Residuals can
83
+ be weighted by the inverse bid-ask spread (default), the inverse vega
84
+ (price space) or equally. Parameters are optimised in a unit cube mapped
85
+ onto the bounds with L-BFGS-B, from several starting points (initial
86
+ guess, optional warm start and Latin-hypercube samples); invalid parameter
87
+ sets (model ``ValueError``) and optional Feller violations are penalised.
88
+ """
89
+
90
+ def __init__(
91
+ self,
92
+ model: BaseModel,
93
+ market_data: pd.DataFrame,
94
+ stock: Stock,
95
+ rate: Rate,
96
+ *,
97
+ objective: str = "iv",
98
+ weighting: str = "spread",
99
+ allow_mc: bool = False,
100
+ feller_penalty: float = 0.0,
101
+ mc_paths: int = 20_000,
102
+ mc_steps: int = 50,
103
+ random_state: int = 0,
104
+ ):
105
+ """
106
+ Initializes the Calibrator.
107
+
108
+ Parameters
109
+ ----------
110
+ model : BaseModel
111
+ The financial model to be calibrated (e.g., HestonModel).
112
+ market_data : pd.DataFrame
113
+ Option quotes: 'strike', 'maturity', 'optionType', 'marketPrice'
114
+ and optionally 'bid'/'ask' (for spread weights) and
115
+ 'impliedVolatility' (warm start for the IV solver).
116
+ stock : Stock
117
+ The underlying asset's properties.
118
+ rate : Rate
119
+ The risk-free rate structure.
120
+ objective : {"iv", "price"}, optional
121
+ Residual space, by default "iv".
122
+ weighting : {"spread", "vega", "none"}, optional
123
+ Residual weights, by default "spread".
124
+ allow_mc : bool, optional
125
+ Permit Monte Carlo pricing for simulation-only models.
126
+ feller_penalty : float, optional
127
+ Weight of the soft Feller penalty (Heston/Bates), by default 0.
128
+ mc_paths, mc_steps : int, optional
129
+ Monte Carlo settings when ``allow_mc`` is used.
130
+ random_state : int, optional
131
+ Seed for the Latin-hypercube starting points, by default 0.
132
+
133
+ Raises
134
+ ------
135
+ NotImplementedError
136
+ If the model cannot be priced on a chain (no closed form, no
137
+ characteristic function and ``allow_mc`` is False).
138
+ """
139
+ if objective not in ("iv", "price"):
140
+ raise ValueError("objective must be 'iv' or 'price'.")
141
+ if weighting not in ("spread", "vega", "none"):
142
+ raise ValueError("weighting must be 'spread', 'vega' or 'none'.")
143
+ if not supports_vectorized_pricing(model, allow_mc=allow_mc):
144
+ raise NotImplementedError(
145
+ f"{model.name} cannot be calibrated: no closed form or "
146
+ "characteristic function (pass allow_mc=True to use Monte Carlo)."
147
+ )
148
+ self.model = model
149
+ self.stock = stock
150
+ self.rate = rate
151
+ self.objective = objective
152
+ self.weighting = weighting
153
+ self.allow_mc = allow_mc
154
+ self.feller_penalty = float(feller_penalty)
155
+ self.mc_kwargs = {"mc_paths": mc_paths, "mc_steps": mc_steps}
156
+ self.random_state = random_state
157
+ self.technique = select_fastest_technique(model)
158
+ self.iv_solver = BSMIVSolver()
159
+ self.market_data = self._prepare(market_data)
160
+ self.result: CalibrationResult | None = None
161
+ self.n_evaluations = 0
162
+ logger.info(
163
+ "Calibrator: %s on %d options, objective=%s, weighting=%s",
164
+ model.name,
165
+ len(self.market_data),
166
+ objective,
167
+ weighting,
168
+ )
169
+
170
+ def _prepare(self, market_data: pd.DataFrame) -> pd.DataFrame:
171
+ data = (
172
+ market_data
173
+ if "iv_market" in market_data.columns
174
+ else annotate_chain(market_data, self.stock, self.rate)
175
+ ).reset_index(drop=True)
176
+ if self.objective == "iv":
177
+ data = data[np.isfinite(data["iv_market"])].reset_index(drop=True)
178
+ self._target_price = data["marketPrice"].to_numpy(dtype=float)
179
+ self._target_iv = data["iv_market"].to_numpy(dtype=float)
180
+ self._weights = self._build_weights(data)
181
+ self._invalid_loss = 1e3 if self.objective == "iv" else 1e6
182
+ self.layout = ChainLayout(data)
183
+ return data
184
+
185
+ def _build_weights(self, data: pd.DataFrame) -> np.ndarray:
186
+ n = len(data)
187
+ if n == 0:
188
+ return np.ones(0)
189
+ if self.weighting == "spread" and {"bid", "ask"} <= set(data.columns):
190
+ spread = (data["ask"] - data["bid"]).to_numpy(dtype=float)
191
+ w = 1.0 / np.maximum(np.nan_to_num(spread, nan=np.inf), 0.01)
192
+ w = np.where(
193
+ np.isfinite(w) & (w > 0),
194
+ w,
195
+ np.nanmin(w[w > 0]) if (w > 0).any() else 1.0,
196
+ )
197
+ elif self.weighting == "vega" and self.objective == "price":
198
+ iv = np.where(np.isfinite(self._target_iv), self._target_iv, 0.2)
199
+ T = data["maturity"].to_numpy(dtype=float)
200
+ K = data["strike"].to_numpy(dtype=float)
201
+ r = np.asarray([self.rate.get_rate(t) for t in T])
202
+ is_call = data["optionType"].to_numpy() == "call"
203
+ _, vega = bsm_prices_and_vegas(
204
+ iv, self.stock.spot, K, T, r, self.stock.dividend, is_call
205
+ )
206
+ w = 1.0 / np.maximum(vega, 1e-2) ** 2
207
+ else:
208
+ w = np.ones(n)
209
+ return w / np.mean(w)
210
+
211
+ # -- objective -----------------------------------------------------------------
212
+ def _prices(self, model: BaseModel) -> np.ndarray:
213
+ return price_chain(
214
+ self.layout,
215
+ self.stock,
216
+ model,
217
+ self.rate,
218
+ allow_mc=self.allow_mc,
219
+ **self.mc_kwargs,
220
+ )
221
+
222
+ def _residuals(self, model: BaseModel) -> np.ndarray:
223
+ prices = self._prices(model)
224
+ if self.objective == "price":
225
+ return prices - self._target_price
226
+ iv = self.iv_solver.solve(
227
+ prices,
228
+ self.market_data,
229
+ self.stock,
230
+ self.rate,
231
+ initial_guess=self._target_iv,
232
+ )
233
+ res = iv - self._target_iv
234
+ # A model price outside the no-arbitrage band has no implied volatility:
235
+ # charge a full vol point so the optimiser moves away from it.
236
+ return np.where(np.isfinite(res), res, 1.0)
237
+
238
+ def _penalty(self, params: dict[str, float]) -> float:
239
+ if self.feller_penalty > 0 and all(k in params for k in _FELLER_KEYS):
240
+ gap = params["vol_of_vol"] ** 2 - 2.0 * params["kappa"] * params["theta"]
241
+ return self.feller_penalty * max(gap, 0.0) ** 2
242
+ return 0.0
243
+
244
+ def loss(self, params: dict[str, float]) -> float:
245
+ """Weighted mean squared residual (plus penalties) for a parameter set."""
246
+ self.n_evaluations += 1
247
+ try:
248
+ model = self.model.with_params(**params)
249
+ except ValueError as e:
250
+ logger.debug("Invalid params %s (%s)", params, e)
251
+ return self._invalid_loss
252
+ try:
253
+ res = self._residuals(model)
254
+ except (ValueError, FloatingPointError, ZeroDivisionError) as e:
255
+ logger.debug("Pricing failed for %s (%s)", params, e)
256
+ return self._invalid_loss
257
+ if not np.all(np.isfinite(res)):
258
+ return self._invalid_loss
259
+ value = float(np.sum(self._weights * res * res) / np.sum(self._weights))
260
+ return value + self._penalty(params)
261
+
262
+ def _objective_function(
263
+ self,
264
+ params_to_fit_values: np.ndarray,
265
+ params_to_fit_names: list[str],
266
+ frozen_params: dict[str, float],
267
+ ) -> float:
268
+ """Total loss for a vector of fitted parameters (legacy entry point)."""
269
+ values = np.asarray(params_to_fit_values, dtype=float)
270
+ params = {**frozen_params, **dict(zip(params_to_fit_names, values))}
271
+ return self.loss(params)
272
+
273
+ # -- fit -----------------------------------------------------------------------
274
+ def fit(
275
+ self,
276
+ initial_guess: dict[str, float],
277
+ bounds: dict[str, tuple],
278
+ frozen_params: dict[str, float] | None = None,
279
+ *,
280
+ n_starts: int = 1,
281
+ warm_start: dict[str, float] | None = None,
282
+ n_local: int = 3,
283
+ time_budget: float | None = None,
284
+ maxiter: int = 200,
285
+ log_scale: Iterable[str] = (),
286
+ ) -> dict[str, float]:
287
+ """
288
+ Performs the calibration.
289
+
290
+ Parameters
291
+ ----------
292
+ initial_guess : dict[str, float]
293
+ Initial guesses for the parameters to be fitted.
294
+ bounds : dict[str, tuple]
295
+ (min, max) bounds per fitted parameter; missing bounds default to
296
+ ±50 % around the guess.
297
+ frozen_params : dict[str, float] | None, optional
298
+ Parameters held constant. Defaults to None.
299
+ n_starts : int, optional
300
+ Total starting points: the guess, the warm start (if given) and
301
+ Latin-hypercube samples filling up to ``n_starts``. Default 1.
302
+ warm_start : dict[str, float] | None, optional
303
+ Previous calibration used as an extra start point.
304
+ n_local : int, optional
305
+ How many of the best-scoring starts get a full local optimisation.
306
+ time_budget : float | None, optional
307
+ Wall-clock seconds; further starts are skipped once exceeded.
308
+ maxiter : int, optional
309
+ L-BFGS-B iteration cap per start, by default 200.
310
+ log_scale : Iterable[str], optional
311
+ Parameters mapped logarithmically onto their (positive) bounds, for
312
+ scales spanning orders of magnitude (e.g. the CEV ``sigma``, whose
313
+ natural size is ``vol * S^(1-gamma)``).
314
+
315
+ Returns
316
+ -------
317
+ dict[str, float]
318
+ The full set of calibrated and frozen parameters. Details are in
319
+ ``self.result`` (:class:`CalibrationResult`).
320
+ """
321
+ t0 = time.perf_counter()
322
+ self.n_evaluations = 0
323
+ frozen = dict(frozen_params or {})
324
+ names = [p for p in initial_guess if p not in frozen]
325
+ if not names:
326
+ self.result = CalibrationResult(
327
+ params=frozen,
328
+ loss=self.loss(frozen) if len(self.market_data) else float("nan"),
329
+ rmse_iv=float("nan"),
330
+ rmse_price=float("nan"),
331
+ n_options=len(self.market_data),
332
+ n_evaluations=self.n_evaluations,
333
+ n_starts=0,
334
+ runtime=time.perf_counter() - t0,
335
+ success=True,
336
+ message="nothing to fit",
337
+ )
338
+ return frozen
339
+ if len(self.market_data) == 0:
340
+ raise ValueError("No options to calibrate to.")
341
+
342
+ lo, hi = self._bounds_arrays(names, initial_guess, bounds)
343
+ is_log = np.array([p in set(log_scale) for p in names])
344
+ if np.any(is_log & (lo <= 0)):
345
+ raise ValueError("log_scale parameters need strictly positive bounds.")
346
+ log_lo = np.log(np.where(is_log, lo, 1.0))
347
+ log_hi = np.log(np.where(is_log, hi, np.e))
348
+
349
+ def to_unit(x: Any) -> np.ndarray:
350
+ x = np.asarray(x, dtype=float)
351
+ linear = (x - lo) / (hi - lo)
352
+ with np.errstate(divide="ignore", invalid="ignore"):
353
+ logarithmic = (np.log(np.where(is_log, x, 1.0)) - log_lo) / (
354
+ log_hi - log_lo
355
+ )
356
+ return np.where(is_log, logarithmic, linear)
357
+
358
+ def from_unit(u: np.ndarray) -> np.ndarray:
359
+ u = np.clip(u, 0.0, 1.0)
360
+ linear = lo + u * (hi - lo)
361
+ logarithmic = np.exp(log_lo + u * (log_hi - log_lo))
362
+ return np.where(is_log, logarithmic, linear)
363
+
364
+ def objective(u: np.ndarray) -> float:
365
+ return self.loss({**frozen, **dict(zip(names, from_unit(u)))})
366
+
367
+ # -- starting points
368
+ starts: list[tuple[np.ndarray, str]] = [
369
+ (np.clip(to_unit([initial_guess[p] for p in names]), 0, 1), "guess")
370
+ ]
371
+ if warm_start and all(p in warm_start for p in names):
372
+ starts.append(
373
+ (np.clip(to_unit([warm_start[p] for p in names]), 0, 1), "warm")
374
+ )
375
+ n_random = max(n_starts - len(starts), 0)
376
+ if n_random:
377
+ sampler = qmc.LatinHypercube(d=len(names), seed=self.random_state)
378
+ for u in sampler.random(n_random):
379
+ starts.append((u, "lhs"))
380
+
381
+ scored = []
382
+ for u, kind in starts:
383
+ scored.append((objective(u), u, kind))
384
+ scored.sort(key=lambda s: s[0])
385
+
386
+ # -- local optimisation from the best starts
387
+ records: list[dict[str, Any]] = []
388
+ best_u, best_loss, best_msg, success = scored[0][1], scored[0][0], "", False
389
+ for i, (loss0, u0, kind) in enumerate(scored[: max(n_local, 1)]):
390
+ if time_budget is not None and i > 0:
391
+ if time.perf_counter() - t0 > time_budget:
392
+ logger.info("Time budget exhausted after %d start(s).", i)
393
+ break
394
+ res = minimize(
395
+ objective,
396
+ x0=u0,
397
+ method="L-BFGS-B",
398
+ bounds=[(0.0, 1.0)] * len(names),
399
+ options={"maxiter": maxiter},
400
+ )
401
+ records.append(
402
+ {
403
+ "kind": kind,
404
+ "start_loss": float(loss0),
405
+ "loss": float(res.fun),
406
+ "params": dict(zip(names, from_unit(res.x))),
407
+ "success": bool(res.success),
408
+ }
409
+ )
410
+ if res.fun < best_loss:
411
+ best_u, best_loss, best_msg = res.x, float(res.fun), str(res.message)
412
+ success = success or bool(res.success)
413
+
414
+ final = {**frozen, **dict(zip(names, from_unit(best_u)))}
415
+ rmse_iv, rmse_price = self._final_rmse(final)
416
+ self.result = CalibrationResult(
417
+ params=final,
418
+ loss=best_loss,
419
+ rmse_iv=rmse_iv,
420
+ rmse_price=rmse_price,
421
+ n_options=len(self.market_data),
422
+ n_evaluations=self.n_evaluations,
423
+ n_starts=len(starts),
424
+ runtime=time.perf_counter() - t0,
425
+ success=success,
426
+ message=best_msg,
427
+ starts=records,
428
+ )
429
+ logger.info(
430
+ "%s calibrated: loss=%.3e rmse_iv=%.4f rmse_price=%.4f "
431
+ "(%d evals, %d starts, %.1fs)",
432
+ self.model.name,
433
+ best_loss,
434
+ rmse_iv,
435
+ rmse_price,
436
+ self.n_evaluations,
437
+ len(starts),
438
+ self.result.runtime,
439
+ )
440
+ return final
441
+
442
+ def _bounds_arrays(
443
+ self,
444
+ names: list[str],
445
+ initial_guess: dict[str, float],
446
+ bounds: dict[str, tuple],
447
+ ) -> tuple[np.ndarray, np.ndarray]:
448
+ lo, hi = [], []
449
+ for p in names:
450
+ b = bounds.get(p)
451
+ if b is None or b[0] is None or b[1] is None:
452
+ g = float(initial_guess[p])
453
+ b = (
454
+ (min(0.5 * g, 1.5 * g), max(0.5 * g, 1.5 * g))
455
+ if g != 0
456
+ else (-1, 1)
457
+ )
458
+ if not b[1] > b[0]:
459
+ raise ValueError(f"Invalid bounds for '{p}': {b}")
460
+ lo.append(float(b[0]))
461
+ hi.append(float(b[1]))
462
+ return np.asarray(lo), np.asarray(hi)
463
+
464
+ def _final_rmse(self, params: dict[str, float]) -> tuple[float, float]:
465
+ try:
466
+ prices = self._prices(self.model.with_params(**params))
467
+ except Exception: # pragma: no cover - defensive
468
+ return float("nan"), float("nan")
469
+ price_err = prices - self._target_price
470
+ iv = self.iv_solver.solve(
471
+ prices,
472
+ self.market_data,
473
+ self.stock,
474
+ self.rate,
475
+ initial_guess=self._target_iv,
476
+ )
477
+ iv_err = iv - self._target_iv
478
+ iv_err = iv_err[np.isfinite(iv_err)]
479
+ rmse_iv = float(np.sqrt(np.mean(iv_err**2))) if iv_err.size else float("nan")
480
+ return rmse_iv, float(np.sqrt(np.mean(price_err**2)))
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
4
+
3
5
  import numpy as np
4
6
  import pandas as pd
5
7
 
@@ -7,6 +9,8 @@ __doc__ = """
7
9
  Provides a utility function to estimate jump parameters from historical data.
8
10
  """
9
11
 
12
+ logger = logging.getLogger(__name__)
13
+
10
14
 
11
15
  def fit_jump_params_from_history(
12
16
  log_returns: pd.Series,
@@ -34,7 +38,7 @@ def fit_jump_params_from_history(
34
38
  A dictionary containing the estimated parameters: 'sigma', 'lambda',
35
39
  'mu_j', and 'sigma_j'.
36
40
  """
37
- print("Fitting jump parameters from historical returns...")
41
+ logger.debug("Fitting jump parameters from historical returns...")
38
42
 
39
43
  std_dev = log_returns.std()
40
44
  jump_threshold = threshold_stds * std_dev
@@ -51,16 +55,16 @@ def fit_jump_params_from_history(
51
55
  sigma_j_est = jump_returns.std()
52
56
  else:
53
57
  lambda_est, mu_j_est, sigma_j_est = 0.1, 0.0, 0.0
54
- print(" -> Warning: Not enough jumps detected. Using default jump parameters.")
58
+ logger.debug("Not enough jumps detected; using default jump parameters.")
55
59
 
56
60
  # Use the key 'sigma' to match the parameter name in the Merton/Kou models.
57
61
  fitted_params = {
58
- "sigma": sigma_est,
59
- "lambda": lambda_est,
60
- "mu_j": mu_j_est,
61
- "sigma_j": sigma_j_est,
62
+ "sigma": float(sigma_est),
63
+ "lambda": float(lambda_est),
64
+ "mu_j": float(mu_j_est),
65
+ "sigma_j": float(sigma_j_est),
62
66
  }
63
67
 
64
68
  formatted = ", ".join(f"{k}: {v:.4f}" for k, v in fitted_params.items())
65
- print(f" -> Estimated Historical Params: {{{formatted}}}")
69
+ logger.debug("Estimated historical params: {%s}", formatted)
66
70
  return fitted_params