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