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