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.
- {optpricing-2.6.0 → optpricing-2.7.0}/CHANGELOG.md +61 -0
- {optpricing-2.6.0/src/optpricing.egg-info → optpricing-2.7.0}/PKG-INFO +3 -2
- {optpricing-2.6.0 → optpricing-2.7.0}/pyproject.toml +3 -1
- optpricing-2.7.0/src/optpricing/calibration/__init__.py +54 -0
- optpricing-2.7.0/src/optpricing/calibration/calibrator.py +480 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/calibration/fit_jump_parameters.py +11 -7
- optpricing-2.7.0/src/optpricing/calibration/fit_market_params.py +271 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/calibration/iv_surface.py +17 -5
- optpricing-2.7.0/src/optpricing/calibration/market_filters.py +307 -0
- optpricing-2.7.0/src/optpricing/calibration/metrics.py +181 -0
- optpricing-2.7.0/src/optpricing/calibration/vectorized_bsm_iv.py +170 -0
- optpricing-2.7.0/src/optpricing/calibration/vectorized_pricer.py +313 -0
- optpricing-2.7.0/src/optpricing/cli/commands/backtest.py +133 -0
- optpricing-2.7.0/src/optpricing/cli/commands/calibrate.py +265 -0
- optpricing-2.7.0/src/optpricing/cli/commands/data.py +243 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/cli/commands/price.py +10 -16
- optpricing-2.7.0/src/optpricing/cli/commands/tune.py +129 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/cli/main.py +8 -2
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/config.py +4 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/dashboard/pages/2_Calibration_and_IV.py +63 -12
- optpricing-2.7.0/src/optpricing/dashboard/service.py +192 -0
- optpricing-2.7.0/src/optpricing/data/__init__.py +53 -0
- optpricing-2.7.0/src/optpricing/data/historical_manager.py +301 -0
- optpricing-2.7.0/src/optpricing/data/market_data_manager.py +636 -0
- optpricing-2.7.0/src/optpricing/data/stream.py +314 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/base/base_model.py +4 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/bates.py +7 -1
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/bsm.py +15 -9
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/cev.py +121 -4
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/merton_jump.py +28 -15
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/sabr.py +24 -14
- optpricing-2.7.0/src/optpricing/workflows/__init__.py +29 -0
- optpricing-2.7.0/src/optpricing/workflows/backtest_workflow.py +299 -0
- optpricing-2.7.0/src/optpricing/workflows/configs/__init__.py +127 -0
- optpricing-2.7.0/src/optpricing/workflows/configs/bsm_config.py +13 -0
- optpricing-2.7.0/src/optpricing/workflows/configs/cev_config.py +23 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/workflows/configs/cgmy_config.py +2 -2
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/workflows/configs/heston_config.py +7 -3
- optpricing-2.7.0/src/optpricing/workflows/configs/kou_config.py +29 -0
- optpricing-2.7.0/src/optpricing/workflows/configs/merton_config.py +29 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/workflows/configs/sabr_jump_config.py +4 -2
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/workflows/configs/vg_config.py +1 -1
- optpricing-2.7.0/src/optpricing/workflows/daily_workflow.py +330 -0
- optpricing-2.7.0/src/optpricing/workflows/tuning.py +269 -0
- {optpricing-2.6.0 → optpricing-2.7.0/src/optpricing.egg-info}/PKG-INFO +3 -2
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing.egg-info/SOURCES.txt +15 -1
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing.egg-info/requires.txt +2 -1
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing.egg-info/scm_file_list.json +26 -0
- optpricing-2.7.0/src/optpricing.egg-info/scm_version.json +8 -0
- optpricing-2.7.0/tests/calibration/test_calibrator.py +191 -0
- optpricing-2.7.0/tests/calibration/test_fit_market_params.py +134 -0
- optpricing-2.7.0/tests/calibration/test_market_filters.py +132 -0
- optpricing-2.7.0/tests/calibration/test_metrics.py +68 -0
- optpricing-2.7.0/tests/calibration/test_vectorized_bsm_iv.py +116 -0
- optpricing-2.7.0/tests/calibration/test_vectorized_pricer.py +186 -0
- optpricing-2.7.0/tests/cli/commands/test_backtest.py +57 -0
- optpricing-2.7.0/tests/cli/commands/test_calibrate.py +120 -0
- optpricing-2.7.0/tests/cli/commands/test_data.py +129 -0
- optpricing-2.7.0/tests/cli/commands/test_price.py +103 -0
- optpricing-2.7.0/tests/cli/commands/test_tune.py +49 -0
- optpricing-2.7.0/tests/conftest.py +101 -0
- optpricing-2.7.0/tests/dashboard/test_pages.py +78 -0
- optpricing-2.7.0/tests/dashboard/test_service.py +166 -0
- optpricing-2.7.0/tests/data/test_historical_manager.py +120 -0
- optpricing-2.7.0/tests/data/test_market_data_manager.py +368 -0
- optpricing-2.7.0/tests/data/test_stream.py +155 -0
- optpricing-2.7.0/tests/models/test_vectorized_closed_forms.py +101 -0
- optpricing-2.7.0/tests/workflows/test_backtest_workflow.py +125 -0
- optpricing-2.7.0/tests/workflows/test_configs.py +74 -0
- optpricing-2.7.0/tests/workflows/test_daily_workflow.py +179 -0
- optpricing-2.7.0/tests/workflows/test_tuning.py +104 -0
- optpricing-2.6.0/src/optpricing/calibration/__init__.py +0 -28
- optpricing-2.6.0/src/optpricing/calibration/calibrator.py +0 -149
- optpricing-2.6.0/src/optpricing/calibration/fit_market_params.py +0 -120
- optpricing-2.6.0/src/optpricing/calibration/vectorized_bsm_iv.py +0 -94
- optpricing-2.6.0/src/optpricing/calibration/vectorized_pricer.py +0 -86
- optpricing-2.6.0/src/optpricing/cli/commands/backtest.py +0 -46
- optpricing-2.6.0/src/optpricing/cli/commands/calibrate.py +0 -145
- optpricing-2.6.0/src/optpricing/cli/commands/data.py +0 -165
- optpricing-2.6.0/src/optpricing/dashboard/service.py +0 -103
- optpricing-2.6.0/src/optpricing/data/__init__.py +0 -28
- optpricing-2.6.0/src/optpricing/data/historical_manager.py +0 -91
- optpricing-2.6.0/src/optpricing/data/market_data_manager.py +0 -200
- optpricing-2.6.0/src/optpricing/workflows/__init__.py +0 -14
- optpricing-2.6.0/src/optpricing/workflows/backtest_workflow.py +0 -128
- optpricing-2.6.0/src/optpricing/workflows/configs/__init__.py +0 -53
- optpricing-2.6.0/src/optpricing/workflows/configs/bsm_config.py +0 -10
- optpricing-2.6.0/src/optpricing/workflows/configs/cev_config.py +0 -16
- optpricing-2.6.0/src/optpricing/workflows/configs/kou_config.py +0 -20
- optpricing-2.6.0/src/optpricing/workflows/configs/merton_config.py +0 -12
- optpricing-2.6.0/src/optpricing/workflows/daily_workflow.py +0 -217
- optpricing-2.6.0/src/optpricing.egg-info/scm_version.json +0 -8
- optpricing-2.6.0/tests/calibration/test_calibrator.py +0 -99
- optpricing-2.6.0/tests/calibration/test_fit_market_params.py +0 -79
- optpricing-2.6.0/tests/calibration/test_vectorized_bsm_iv.py +0 -47
- optpricing-2.6.0/tests/calibration/test_vectorized_pricer.py +0 -75
- optpricing-2.6.0/tests/cli/commands/test_backtest.py +0 -20
- optpricing-2.6.0/tests/cli/commands/test_calibrate.py +0 -39
- optpricing-2.6.0/tests/cli/commands/test_data.py +0 -53
- optpricing-2.6.0/tests/cli/commands/test_price.py +0 -60
- optpricing-2.6.0/tests/dashboard/test_service.py +0 -114
- optpricing-2.6.0/tests/data/test_historical_manager.py +0 -58
- optpricing-2.6.0/tests/data/test_market_data_manager.py +0 -193
- optpricing-2.6.0/tests/workflows/test_backtest_workflow.py +0 -73
- optpricing-2.6.0/tests/workflows/test_daily_workflow.py +0 -76
- {optpricing-2.6.0 → optpricing-2.7.0}/LICENSE +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/MANIFEST.in +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/README.md +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/benchmarks/run_benchmarks.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/setup.cfg +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/__init__.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/atoms/__init__.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/atoms/bond.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/atoms/option.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/atoms/rate.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/atoms/stock.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/calibration/technique_selector.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/calibration/vectorized_integration_iv.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/cli/__init__.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/cli/commands/__init__.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/cli/commands/dashboard.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/cli/commands/demo.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/cli/commands/tools.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/dashboard/Home.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/dashboard/__init__.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/dashboard/pages/1_Pricer_and_Greeks.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/dashboard/pages/3_Market_Analytics.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/dashboard/pages/4_Model_Fitting.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/dashboard/pages/5_Term_Structure.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/dashboard/pages/__init__.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/dashboard/plots.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/dashboard/widgets.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/__init__.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/base/__init__.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/base/cf_sampler.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/base/validators.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/blacks_approx.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/cgmy.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/cir.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/dupire_local.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/heston.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/hyperbolic.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/kou.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/nig.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/perpetual_put.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/sabr_jump.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/vasicek.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/models/vg.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/parity/__init__.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/parity/implied_rate.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/parity/parity_model.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/__init__.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/american_monte_carlo.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/base/__init__.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/base/base_technique.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/base/greek_mixin.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/base/iv_mixin.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/base/lattice_technique.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/base/pricing_result.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/base/random_utils.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/closed_form.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/crr.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/fft.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/integration.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/kernels/__init__.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/kernels/american_mc_kernels.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/kernels/lattice_kernels.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/kernels/mc_kernels.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/kernels/path_kernels.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/leisen_reimer.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/monte_carlo.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/pde.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/techniques/topm.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/workflows/configs/bates_config.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/workflows/configs/hyperbolic_config.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/workflows/configs/nig_config.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing/workflows/configs/sabr_config.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing.egg-info/dependency_links.txt +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing.egg-info/entry_points.txt +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/src/optpricing.egg-info/top_level.txt +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/atoms/test_bond.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/atoms/test_option.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/atoms/test_rate.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/atoms/test_stock.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/calibration/test_fit_jump_parameters.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/calibration/test_iv_surface.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/calibration/test_technique_selector.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/calibration/test_vectorized_integration_iv.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/cli/commands/test_tools.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/cli/test_main.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/correctness/__init__.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/correctness/_helpers.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/correctness/conftest.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/correctness/test_invariants.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/correctness/test_mc_regressions.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/correctness/test_reference_values.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/dashboard/test_plots.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/dashboard/test_widgets.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/base/test_base_model.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/base/test_validators.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_bates.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_blacks_approx.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_bsm.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_cev.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_cgmy.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_cir.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_dupire_local.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_heston.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_hyperbolic.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_kou.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_merton_jump.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_nig.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_perpetual_put.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_sabr.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_sabr_jump.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_vasicek.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/models/test_vg.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/parity/test_implied_rate.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/parity/test_parity_model.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/base/test_greek_mixin.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/base/test_iv_mixin.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/base/test_pricing_result.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/base/test_random_utils.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/kernels/test_lattice_kernels.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/kernels/test_mc_kernels.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/kernels/test_path_kernels.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/test_american_monte_carlo.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/test_closed_form.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/test_crr.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/test_fft.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/test_integration.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/test_leisen_reimer.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/test_monte_carlo.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/test_pde.py +0 -0
- {optpricing-2.6.0 → optpricing-2.7.0}/tests/techniques/test_topm.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
+
logger.debug("Estimated historical params: {%s}", formatted)
|
|
66
70
|
return fitted_params
|