quantark 0.1.0__py3-none-any.whl
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.
- quantark/__init__.py +3 -0
- quantark/_compat.py +150 -0
- quantark/asset/__init__.py +8 -0
- quantark/asset/bond/__init__.py +2 -0
- quantark/asset/bond/engine/__init__.py +44 -0
- quantark/asset/bond/engine/analytical/__init__.py +12 -0
- quantark/asset/bond/engine/analytical/black_engine.py +583 -0
- quantark/asset/bond/engine/analytical/bond_forward_engine.py +390 -0
- quantark/asset/bond/engine/analytical/bond_futures_engine.py +569 -0
- quantark/asset/bond/engine/convertible/__init__.py +12 -0
- quantark/asset/bond/engine/convertible/convertible_bond_engine.py +800 -0
- quantark/asset/bond/engine/discount/__init__.py +10 -0
- quantark/asset/bond/engine/discount/bond_discount_engine.py +517 -0
- quantark/asset/bond/engine/discount/frn_engine.py +913 -0
- quantark/asset/bond/engine/pde/__init__.py +14 -0
- quantark/asset/bond/engine/pde/convertible/__init__.py +21 -0
- quantark/asset/bond/engine/pde/convertible/jump_diffusion_engine.py +603 -0
- quantark/asset/bond/engine/pde/convertible/pde_params.py +59 -0
- quantark/asset/bond/engine/pde/convertible/tf_engine.py +546 -0
- quantark/asset/bond/engine/tree/__init__.py +14 -0
- quantark/asset/bond/engine/tree/convertible/__init__.py +21 -0
- quantark/asset/bond/engine/tree/convertible/binomial_engine.py +488 -0
- quantark/asset/bond/engine/tree/convertible/tree_params.py +72 -0
- quantark/asset/bond/engine/tree/convertible/trinomial_engine.py +1341 -0
- quantark/asset/bond/product/__init__.py +37 -0
- quantark/asset/bond/product/base_bond_product.py +114 -0
- quantark/asset/bond/product/convertible/__init__.py +16 -0
- quantark/asset/bond/product/convertible/convertible_bond.py +595 -0
- quantark/asset/bond/product/couponbond/__init__.py +12 -0
- quantark/asset/bond/product/couponbond/fixed_bond.py +285 -0
- quantark/asset/bond/product/couponbond/frn.py +538 -0
- quantark/asset/bond/product/forward/__init__.py +9 -0
- quantark/asset/bond/product/forward/base_bond_forward.py +92 -0
- quantark/asset/bond/product/forward/bond_forward.py +335 -0
- quantark/asset/bond/product/futures/__init__.py +8 -0
- quantark/asset/bond/product/futures/bond_futures.py +532 -0
- quantark/asset/bond/product/option/__init__.py +9 -0
- quantark/asset/bond/product/option/euro_short_term_bond_option.py +231 -0
- quantark/asset/bond/riskmeasures/__init__.py +13 -0
- quantark/asset/bond/riskmeasures/bond_greeks_calculator.py +484 -0
- quantark/asset/bond/schedule/__init__.py +21 -0
- quantark/asset/bond/schedule/cashflow.py +595 -0
- quantark/asset/equity/__init__.py +11 -0
- quantark/asset/equity/analysis/__init__.py +4 -0
- quantark/asset/equity/analysis/autocallable_path_analyzer.py +257 -0
- quantark/asset/equity/engine/__init__.py +84 -0
- quantark/asset/equity/engine/analytical/__init__.py +37 -0
- quantark/asset/equity/engine/analytical/american_option_engine.py +682 -0
- quantark/asset/equity/engine/analytical/asian_option_analytical_engine.py +1102 -0
- quantark/asset/equity/engine/analytical/barrier_analytical_engine.py +455 -0
- quantark/asset/equity/engine/analytical/black_scholes_engine.py +322 -0
- quantark/asset/equity/engine/analytical/deltaone_engine.py +340 -0
- quantark/asset/equity/engine/analytical/digital_option_engine.py +168 -0
- quantark/asset/equity/engine/analytical/double_barrier_option_engine.py +481 -0
- quantark/asset/equity/engine/analytical/double_sharkfin_option_analytical_engine.py +508 -0
- quantark/asset/equity/engine/analytical/one_touch_analytical_engine.py +302 -0
- quantark/asset/equity/engine/analytical/range_accrual_analytical_engine.py +396 -0
- quantark/asset/equity/engine/analytical/single_sharkfin_option_analytical_engine.py +229 -0
- quantark/asset/equity/engine/base_engine.py +137 -0
- quantark/asset/equity/engine/event_stats.py +85 -0
- quantark/asset/equity/engine/mc/__init__.py +31 -0
- quantark/asset/equity/engine/mc/american_option_mc_engine.py +485 -0
- quantark/asset/equity/engine/mc/asian_option_mc_engine.py +678 -0
- quantark/asset/equity/engine/mc/barrier_option_mc_engine.py +726 -0
- quantark/asset/equity/engine/mc/digital_option_mc_engine.py +419 -0
- quantark/asset/equity/engine/mc/double_sharkfin_option_mc_engine.py +676 -0
- quantark/asset/equity/engine/mc/euro_mc_engine.py +423 -0
- quantark/asset/equity/engine/mc/phoenix_mc_engine.py +1206 -0
- quantark/asset/equity/engine/mc/range_accrual_mc_engine.py +738 -0
- quantark/asset/equity/engine/mc/single_sharkfin_option_mc_engine.py +549 -0
- quantark/asset/equity/engine/mc/snowball_mc_engine.py +2250 -0
- quantark/asset/equity/engine/pde/__init__.py +36 -0
- quantark/asset/equity/engine/pde/american_pde_solver.py +211 -0
- quantark/asset/equity/engine/pde/barrier_pde_solver.py +692 -0
- quantark/asset/equity/engine/pde/base_pde_solver.py +994 -0
- quantark/asset/equity/engine/pde/double_barrier_pde_solver.py +510 -0
- quantark/asset/equity/engine/pde/double_one_touch_pde_solver.py +435 -0
- quantark/asset/equity/engine/pde/european_pde_solver.py +170 -0
- quantark/asset/equity/engine/pde/ko_reset_snowball_pde_solver.py +477 -0
- quantark/asset/equity/engine/pde/one_touch_pde_solver.py +439 -0
- quantark/asset/equity/engine/pde/phoenix_pde_solver.py +613 -0
- quantark/asset/equity/engine/pde/snowball_pde_solver.py +1810 -0
- quantark/asset/equity/engine/pde/spatial_grid.py +750 -0
- quantark/asset/equity/engine/pde/time_grid.py +308 -0
- quantark/asset/equity/engine/pde_engine.py +238 -0
- quantark/asset/equity/engine/quad/__init__.py +23 -0
- quantark/asset/equity/engine/quad/discrete_quad_engine.py +106 -0
- quantark/asset/equity/engine/quad/european_quad_engine.py +325 -0
- quantark/asset/equity/engine/quad/ko_reset_snowball_quad_engine.py +362 -0
- quantark/asset/equity/engine/quad/phoenix_quad_engine.py +614 -0
- quantark/asset/equity/engine/quad/quad_adapters.py +1260 -0
- quantark/asset/equity/engine/quad/quad_core.py +513 -0
- quantark/asset/equity/engine/quad/quad_math.py +219 -0
- quantark/asset/equity/engine/quad/snowball_quad_engine.py +1137 -0
- quantark/asset/equity/engine/validation/script/benchmark_check_american_analytical.py +117 -0
- quantark/asset/equity/engine/validation/script/benchmark_check_american_pde.py +114 -0
- quantark/asset/equity/engine/validation/script/benchmark_check_asian_analytical.py +440 -0
- quantark/asset/equity/engine/validation/script/benchmark_check_barrier_analytical.py +269 -0
- quantark/asset/equity/engine/validation/script/benchmark_check_barrier_pde_solver.py +636 -0
- quantark/asset/equity/engine/validation/script/benchmark_check_digital_option.py +256 -0
- quantark/asset/equity/engine/validation/script/benchmark_check_snowball_pde_solver.py +807 -0
- quantark/asset/equity/engine/validation/script/boundary_check_american_analytical.py +290 -0
- quantark/asset/equity/engine/validation/script/boundary_check_american_pde.py +242 -0
- quantark/asset/equity/engine/validation/script/boundary_check_asian_analytical.py +612 -0
- quantark/asset/equity/engine/validation/script/boundary_check_barrier_analytical.py +434 -0
- quantark/asset/equity/engine/validation/script/boundary_check_barrier_pde_solver.py +748 -0
- quantark/asset/equity/engine/validation/script/boundary_check_digital_option.py +575 -0
- quantark/asset/equity/engine/validation/script/boundary_check_snowball_pde_solver.py +1101 -0
- quantark/asset/equity/engine/validation/script/greeks_check_digital_option.py +349 -0
- quantark/asset/equity/engine/validation/script/mc_comparison_barrier_pde.py +270 -0
- quantark/asset/equity/engine/validation/script/quick_mc_compare.py +51 -0
- quantark/asset/equity/engine/validation/script/validation_stepdown_improved.py +97 -0
- quantark/asset/equity/param/__init__.py +24 -0
- quantark/asset/equity/param/engine_param_profiles.py +325 -0
- quantark/asset/equity/param/engine_params.py +728 -0
- quantark/asset/equity/process/__init__.py +7 -0
- quantark/asset/equity/process/bsm/__init__.py +7 -0
- quantark/asset/equity/process/bsm/bsm_process.py +108 -0
- quantark/asset/equity/process/bsm/qmc_brownian_bridge.py +401 -0
- quantark/asset/equity/process/bsm/qmc_path_generator.py +694 -0
- quantark/asset/equity/process/bsm/qmc_rqmc_driver.py +163 -0
- quantark/asset/equity/process/bsm/qmc_sobol.py +195 -0
- quantark/asset/equity/process/bsm/qmc_variance_reduction.py +292 -0
- quantark/asset/equity/product/__init__.py +8 -0
- quantark/asset/equity/product/base_equity_product.py +72 -0
- quantark/asset/equity/product/deltaone/__init__.py +22 -0
- quantark/asset/equity/product/deltaone/base_deltaone_product.py +147 -0
- quantark/asset/equity/product/deltaone/futures.py +485 -0
- quantark/asset/equity/product/deltaone/spot_instrument.py +118 -0
- quantark/asset/equity/product/option/__init__.py +104 -0
- quantark/asset/equity/product/option/american_option.py +114 -0
- quantark/asset/equity/product/option/asian_option.py +531 -0
- quantark/asset/equity/product/option/barrier_option.py +289 -0
- quantark/asset/equity/product/option/base_equity_option.py +659 -0
- quantark/asset/equity/product/option/digital_option.py +102 -0
- quantark/asset/equity/product/option/double_barrier_option.py +286 -0
- quantark/asset/equity/product/option/double_one_touch_option.py +310 -0
- quantark/asset/equity/product/option/double_sharkfin_option.py +466 -0
- quantark/asset/equity/product/option/european_vanilla_option.py +103 -0
- quantark/asset/equity/product/option/ko_reset_snowball_option.py +563 -0
- quantark/asset/equity/product/option/observation_schedule.py +530 -0
- quantark/asset/equity/product/option/one_touch_option.py +287 -0
- quantark/asset/equity/product/option/phoenix_config.py +116 -0
- quantark/asset/equity/product/option/phoenix_helpers.py +576 -0
- quantark/asset/equity/product/option/phoenix_option.py +1167 -0
- quantark/asset/equity/product/option/range_accrual_config.py +288 -0
- quantark/asset/equity/product/option/range_accrual_helpers.py +608 -0
- quantark/asset/equity/product/option/range_accrual_option.py +526 -0
- quantark/asset/equity/product/option/single_sharkfin_option.py +420 -0
- quantark/asset/equity/product/option/snowball_config.py +261 -0
- quantark/asset/equity/product/option/snowball_helpers.py +977 -0
- quantark/asset/equity/product/option/snowball_option.py +1242 -0
- quantark/asset/equity/report/__init__.py +15 -0
- quantark/asset/equity/report/autocallable_risk_report.py +2118 -0
- quantark/asset/equity/report/plotting.py +87 -0
- quantark/asset/equity/report/snowball_risk_comparison_report.py +2230 -0
- quantark/asset/equity/report/surfaces.py +123 -0
- quantark/asset/equity/report/term_structure.py +126 -0
- quantark/asset/equity/riskmeasures/__init__.py +7 -0
- quantark/asset/equity/riskmeasures/greeks_calculator.py +1204 -0
- quantark/asset/rate/__init__.py +58 -0
- quantark/asset/rate/engine/__init__.py +25 -0
- quantark/asset/rate/engine/cap_floor_engine.py +514 -0
- quantark/asset/rate/engine/fra_engine.py +286 -0
- quantark/asset/rate/engine/irs_discount_engine.py +891 -0
- quantark/asset/rate/engine/swaption_engine.py +587 -0
- quantark/asset/rate/product/__init__.py +67 -0
- quantark/asset/rate/product/cap_floor.py +550 -0
- quantark/asset/rate/product/fra.py +219 -0
- quantark/asset/rate/product/irs.py +1223 -0
- quantark/asset/rate/product/swaption.py +372 -0
- quantark/backtest/__init__.py +153 -0
- quantark/backtest/base.py +263 -0
- quantark/backtest/dashboard.py +874 -0
- quantark/backtest/equity/__init__.py +35 -0
- quantark/backtest/equity/config.py +118 -0
- quantark/backtest/equity/engine.py +408 -0
- quantark/backtest/equity/hedge_executor.py +374 -0
- quantark/backtest/equity/metrics.py +396 -0
- quantark/backtest/equity/results.py +232 -0
- quantark/backtest/equity/state.py +252 -0
- quantark/backtest/examples/__init__.py +4 -0
- quantark/backtest/examples/advanced_backtest.py +345 -0
- quantark/backtest/examples/basic_delta_hedge.py +246 -0
- quantark/backtest/examples/fi_dv01_hedge.py +267 -0
- quantark/backtest/fi/__init__.py +30 -0
- quantark/backtest/fi/config.py +114 -0
- quantark/backtest/fi/engine.py +378 -0
- quantark/backtest/fi/hedge_executor.py +254 -0
- quantark/backtest/fi/metrics.py +308 -0
- quantark/backtest/fi/results.py +193 -0
- quantark/backtest/fi/state.py +212 -0
- quantark/backtest/logger.py +393 -0
- quantark/backtest/otc/__init__.py +74 -0
- quantark/backtest/otc/_replay.py +637 -0
- quantark/backtest/otc/book_engine.py +587 -0
- quantark/backtest/otc/config.py +175 -0
- quantark/backtest/otc/dashboard.py +1006 -0
- quantark/backtest/otc/engine.py +420 -0
- quantark/backtest/otc/engine_factory.py +138 -0
- quantark/backtest/otc/market.py +216 -0
- quantark/backtest/otc/results.py +107 -0
- quantark/backtest/otc/state.py +166 -0
- quantark/backtest/report_generator.py +608 -0
- quantark/backtest/strategy/__init__.py +28 -0
- quantark/backtest/strategy/base_strategy.py +235 -0
- quantark/backtest/strategy/convexity_neutral_strategy.py +247 -0
- quantark/backtest/strategy/delta_neutral_strategy.py +283 -0
- quantark/backtest/strategy/dv01_neutral_strategy.py +283 -0
- quantark/backtest/transaction_costs.py +485 -0
- quantark/backtest/visualizer.py +1019 -0
- quantark/cashleg/__init__.py +31 -0
- quantark/cashleg/accrual_leg.py +120 -0
- quantark/cashleg/base.py +48 -0
- quantark/cashleg/base_amount.py +60 -0
- quantark/cashleg/deterministic_leg.py +39 -0
- quantark/cashleg/event_distribution.py +262 -0
- quantark/cashleg/fixed_payoff_leg.py +92 -0
- quantark/cashleg/leg_schedule.py +95 -0
- quantark/cashleg/leg_valuator.py +40 -0
- quantark/dynamicscenario/__init__.py +97 -0
- quantark/dynamicscenario/base.py +297 -0
- quantark/dynamicscenario/config.py +122 -0
- quantark/dynamicscenario/engine.py +703 -0
- quantark/dynamicscenario/equity/__init__.py +14 -0
- quantark/dynamicscenario/fi/__init__.py +24 -0
- quantark/dynamicscenario/fi/config.py +149 -0
- quantark/dynamicscenario/fi/engine.py +500 -0
- quantark/dynamicscenario/fi/results.py +503 -0
- quantark/dynamicscenario/path/__init__.py +17 -0
- quantark/dynamicscenario/path/day_path.py +397 -0
- quantark/dynamicscenario/path/fi_path_library.py +488 -0
- quantark/dynamicscenario/path/path_builder.py +726 -0
- quantark/dynamicscenario/path/path_library.py +620 -0
- quantark/dynamicscenario/report/__init__.py +12 -0
- quantark/dynamicscenario/report/dynamic_report.py +1175 -0
- quantark/dynamicscenario/report/visualizer.py +1586 -0
- quantark/dynamicscenario/results/__init__.py +19 -0
- quantark/dynamicscenario/results/dynamic_results.py +579 -0
- quantark/dynamicscenario/results/result_exporter.py +438 -0
- quantark/param/__init__.py +75 -0
- quantark/param/basis/__init__.py +19 -0
- quantark/param/basis/basis_yield.py +301 -0
- quantark/param/div/__init__.py +16 -0
- quantark/param/div/dividend_yield.py +123 -0
- quantark/param/index/__init__.py +52 -0
- quantark/param/index/rate_index.py +568 -0
- quantark/param/quote/__init__.py +7 -0
- quantark/param/quote/spot_quote.py +35 -0
- quantark/param/rrf/__init__.py +22 -0
- quantark/param/rrf/rate_curve.py +436 -0
- quantark/param/vol/__init__.py +6 -0
- quantark/param/vol/vol_surface.py +118 -0
- quantark/portfolio/__init__.py +61 -0
- quantark/portfolio/base.py +203 -0
- quantark/portfolio/equity/__init__.py +17 -0
- quantark/portfolio/equity/portfolio.py +391 -0
- quantark/portfolio/equity/position.py +368 -0
- quantark/portfolio/fi/__init__.py +14 -0
- quantark/portfolio/fi/portfolio.py +424 -0
- quantark/portfolio/fi/position.py +272 -0
- quantark/portfolio/portfolio_snapshot.py +221 -0
- quantark/portfolio/portfolio_storage.py +414 -0
- quantark/priceenv/__init__.py +7 -0
- quantark/priceenv/pricing_environment.py +196 -0
- quantark/rfq/__init__.py +32 -0
- quantark/rfq/builders.py +102 -0
- quantark/rfq/models.py +214 -0
- quantark/rfq/registry.py +611 -0
- quantark/rfq/service.py +237 -0
- quantark/simm/__init__.py +155 -0
- quantark/simm/calibration/__init__.py +206 -0
- quantark/simm/calibration/accessors.py +439 -0
- quantark/simm/calibration/commodity.py +156 -0
- quantark/simm/calibration/credit_non_qualifying.py +79 -0
- quantark/simm/calibration/credit_qualifying.py +130 -0
- quantark/simm/calibration/cross_risk.py +39 -0
- quantark/simm/calibration/equity.py +125 -0
- quantark/simm/calibration/fx.py +92 -0
- quantark/simm/calibration/ir.py +152 -0
- quantark/simm/calibration/version.py +33 -0
- quantark/simm/config.py +186 -0
- quantark/simm/crif/__init__.py +35 -0
- quantark/simm/crif/models.py +230 -0
- quantark/simm/crif/parser.py +585 -0
- quantark/simm/engines/__init__.py +62 -0
- quantark/simm/engines/aggregation/__init__.py +67 -0
- quantark/simm/engines/aggregation/addon.py +141 -0
- quantark/simm/engines/aggregation/bucket_aggregator.py +298 -0
- quantark/simm/engines/aggregation/concentration.py +349 -0
- quantark/simm/engines/aggregation/product_class_aggregator.py +183 -0
- quantark/simm/engines/aggregation/risk_class_aggregator.py +403 -0
- quantark/simm/engines/aggregation/simm_calculator.py +430 -0
- quantark/simm/engines/aggregation/weighted_sensitivity.py +272 -0
- quantark/simm/engines/base.py +231 -0
- quantark/simm/engines/classification/__init__.py +10 -0
- quantark/simm/engines/classification/bucket_mapper.py +347 -0
- quantark/simm/engines/factory.py +137 -0
- quantark/simm/engines/portfolio_adapter.py +336 -0
- quantark/simm/engines/result.py +176 -0
- quantark/simm/engines/risk_class/__init__.py +18 -0
- quantark/simm/engines/risk_class/equity_engine.py +263 -0
- quantark/simm/engines/risk_class/ir_engine.py +264 -0
- quantark/simm/report/__init__.py +17 -0
- quantark/simm/report/crif_export.py +284 -0
- quantark/simm/report/excel_generator.py +401 -0
- quantark/simm/report/html_generator.py +840 -0
- quantark/simm/results/__init__.py +38 -0
- quantark/simm/results/attribution.py +313 -0
- quantark/simm/results/simm_result.py +339 -0
- quantark/simm/results/whatif.py +268 -0
- quantark/simm/sensitivity.py +533 -0
- quantark/simm/taxonomy.py +416 -0
- quantark/stresstest/__init__.py +67 -0
- quantark/stresstest/base.py +116 -0
- quantark/stresstest/config.py +5 -0
- quantark/stresstest/engine.py +5 -0
- quantark/stresstest/equity/__init__.py +17 -0
- quantark/stresstest/equity/config.py +69 -0
- quantark/stresstest/equity/engine.py +272 -0
- quantark/stresstest/equity/report/__init__.py +7 -0
- quantark/stresstest/equity/report/report_generator.py +423 -0
- quantark/stresstest/equity/report/visualizer.py +328 -0
- quantark/stresstest/equity/results.py +145 -0
- quantark/stresstest/fi/__init__.py +15 -0
- quantark/stresstest/fi/config.py +59 -0
- quantark/stresstest/fi/engine.py +213 -0
- quantark/stresstest/fi/metrics.py +60 -0
- quantark/stresstest/fi/results.py +64 -0
- quantark/stresstest/report/__init__.py +12 -0
- quantark/stresstest/report/report_generator.py +5 -0
- quantark/stresstest/report/visualizer.py +5 -0
- quantark/stresstest/results/__init__.py +16 -0
- quantark/stresstest/results/result_aggregator.py +325 -0
- quantark/stresstest/results/result_exporter.py +286 -0
- quantark/stresstest/results/stress_results.py +5 -0
- quantark/stresstest/scenario/__init__.py +13 -0
- quantark/stresstest/scenario/scenario.py +242 -0
- quantark/stresstest/scenario/scenario_builder.py +376 -0
- quantark/stresstest/scenario/scenario_library.py +435 -0
- quantark/stresstest/scenario/scenario_storage.py +224 -0
- quantark/stresstest/stress/__init__.py +13 -0
- quantark/stresstest/stress/stress_applicator.py +590 -0
- quantark/stresstest/stress/stress_types.py +142 -0
- quantark/util/__init__.py +23 -0
- quantark/util/barrier_shift.py +44 -0
- quantark/util/calendar/__init__.py +27 -0
- quantark/util/calendar/business_calendar.py +584 -0
- quantark/util/calendar/day_counter.py +517 -0
- quantark/util/calendar/holidayfile/china.csv +1920 -0
- quantark/util/calendar/holidayfile/china_sse.csv +1462 -0
- quantark/util/enum/__init__.py +81 -0
- quantark/util/enum/bond_enums.py +112 -0
- quantark/util/enum/deltaone_enums.py +16 -0
- quantark/util/enum/engine_enums.py +137 -0
- quantark/util/enum/greeks_enums.py +29 -0
- quantark/util/enum/option_enums.py +221 -0
- quantark/util/exceptions.py +66 -0
- quantark/util/marketdata/__init__.py +39 -0
- quantark/util/marketdata/adapter/base_adapter.py +203 -0
- quantark/util/marketdata/adapter/mock_adapter.py +265 -0
- quantark/util/marketdata/converter.py +289 -0
- quantark/util/marketdata/example_usage.py +314 -0
- quantark/util/marketdata/generator/__init__.py +7 -0
- quantark/util/marketdata/generator/mock_generator.py +466 -0
- quantark/util/marketdata/models.py +358 -0
- quantark/util/marketdata/storage/__init__.py +7 -0
- quantark/util/marketdata/storage/parquet_storage.py +340 -0
- quantark/util/numerical/__init__.py +98 -0
- quantark/util/numerical/comparison.py +219 -0
- quantark/util/numerical/constants.py +98 -0
- quantark/util/numerical/formatting.py +380 -0
- quantark/util/numerical/pnl.py +17 -0
- quantark/util/numerical/safe_math.py +238 -0
- quantark/util/numerical/validation.py +315 -0
- quantark/var/__init__.py +39 -0
- quantark/var/attribution.py +398 -0
- quantark/var/backtest/__init__.py +7 -0
- quantark/var/backtest/var_backtester.py +309 -0
- quantark/var/base.py +63 -0
- quantark/var/config.py +219 -0
- quantark/var/engines/__init__.py +13 -0
- quantark/var/engines/historical.py +925 -0
- quantark/var/engines/monte_carlo.py +870 -0
- quantark/var/engines/parametric.py +1199 -0
- quantark/var/results/__init__.py +16 -0
- quantark/var/results/incremental_var_result.py +131 -0
- quantark/var/results/var_report.py +346 -0
- quantark/var/results/var_result.py +134 -0
- quantark/var/risk_factors/__init__.py +22 -0
- quantark/var/risk_factors/base.py +41 -0
- quantark/var/risk_factors/equity_factors.py +158 -0
- quantark/var/risk_factors/fi_factors.py +99 -0
- quantark-0.1.0.dist-info/METADATA +351 -0
- quantark-0.1.0.dist-info/RECORD +399 -0
- quantark-0.1.0.dist-info/WHEEL +4 -0
- quantark-0.1.0.dist-info/licenses/LICENSE +202 -0
- quantark-0.1.0.dist-info/licenses/NOTICE +2 -0
- quantark_compat.pth +1 -0
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Spatial grid utilities for PDE solvers.
|
|
3
|
+
|
|
4
|
+
Provides methods for generating spatial discretizations in log-price space,
|
|
5
|
+
including the Tavella-Randall transformation for concentrating grid points
|
|
6
|
+
near critical prices (strikes, barriers, etc.).
|
|
7
|
+
|
|
8
|
+
The beta parameter in Tavella-Randall controls grid concentration:
|
|
9
|
+
- Smaller beta = more concentration near critical point
|
|
10
|
+
- Larger beta = more uniform grid
|
|
11
|
+
|
|
12
|
+
Beta is automatically calculated to achieve a target local spacing (epsilon)
|
|
13
|
+
near the critical point using bisection. Default epsilon values:
|
|
14
|
+
- Pricing only: 0.5% (eps_crit=0.005)
|
|
15
|
+
- Pricing + Greeks: 0.2-0.3% (eps_crit=0.002-0.003)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
from typing import List, Optional, Tuple
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SpatialGrid:
|
|
23
|
+
"""
|
|
24
|
+
Static utility class for generating spatial grids.
|
|
25
|
+
|
|
26
|
+
All methods work in log-price space (x = ln(S)) for numerical stability.
|
|
27
|
+
The Tavella-Randall transformation allows concentration of grid points
|
|
28
|
+
near critical prices like strikes and barriers.
|
|
29
|
+
|
|
30
|
+
The transformation is:
|
|
31
|
+
x(w) = x_crit + beta * sinh(c1*(1-w) + c2*w), w in [0, 1]
|
|
32
|
+
|
|
33
|
+
where:
|
|
34
|
+
c1 = arcsinh((x_min - x_crit) / beta)
|
|
35
|
+
c2 = arcsinh((x_max - x_crit) / beta)
|
|
36
|
+
|
|
37
|
+
The local spacing near the critical point is approximately:
|
|
38
|
+
dx_crit ≈ beta * (c2 - c1) / N
|
|
39
|
+
|
|
40
|
+
Beta is chosen to achieve a target relative spacing epsilon:
|
|
41
|
+
dx_crit ≈ ln(1 + epsilon) ≈ epsilon
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# Default target relative spacing near critical point
|
|
45
|
+
# 0.3% gives good pricing + Greeks accuracy
|
|
46
|
+
DEFAULT_EPSILON_CRIT = 0.003
|
|
47
|
+
|
|
48
|
+
# Maximum allowed ratio of max/min grid spacing (tail coarseness guard)
|
|
49
|
+
DEFAULT_MAX_DX_RATIO = 100.0
|
|
50
|
+
|
|
51
|
+
# Bisection parameters for beta calculation
|
|
52
|
+
_BISECTION_ITERS = 80
|
|
53
|
+
_BETA_LO = 1e-12
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _beta_for_target_dx(
|
|
57
|
+
x_min: float,
|
|
58
|
+
x_max: float,
|
|
59
|
+
x_crit: float,
|
|
60
|
+
num_points: int,
|
|
61
|
+
dx_target: float,
|
|
62
|
+
beta_lo: float = _BETA_LO,
|
|
63
|
+
beta_hi: Optional[float] = None,
|
|
64
|
+
iters: int = _BISECTION_ITERS,
|
|
65
|
+
) -> float:
|
|
66
|
+
"""
|
|
67
|
+
Solve for beta to achieve a target local spacing near the critical point.
|
|
68
|
+
|
|
69
|
+
Uses bisection to find beta such that:
|
|
70
|
+
dx_crit = beta * (c2 - c1) / N ≈ dx_target
|
|
71
|
+
|
|
72
|
+
where N = num_points - 1, and c1, c2 are the Tavella-Randall parameters.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
x_min: Minimum log-price
|
|
76
|
+
x_max: Maximum log-price
|
|
77
|
+
x_crit: Critical log-price (concentration point)
|
|
78
|
+
num_points: Number of grid points
|
|
79
|
+
dx_target: Target local spacing near critical point (in log-space)
|
|
80
|
+
beta_lo: Lower bound for bisection (default: 1e-12)
|
|
81
|
+
beta_hi: Upper bound for bisection (default: auto-calculated)
|
|
82
|
+
iters: Maximum bisection iterations (default: 80)
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Beta value that achieves (approximately) the target spacing
|
|
86
|
+
"""
|
|
87
|
+
N = num_points - 1
|
|
88
|
+
if beta_hi is None:
|
|
89
|
+
# Large enough that grid is near-uniform
|
|
90
|
+
beta_hi = 1e3 * (x_max - x_min)
|
|
91
|
+
|
|
92
|
+
def dxcrit(beta: float) -> float:
|
|
93
|
+
"""Compute local spacing at critical point for given beta."""
|
|
94
|
+
c1 = np.arcsinh((x_min - x_crit) / beta)
|
|
95
|
+
c2 = np.arcsinh((x_max - x_crit) / beta)
|
|
96
|
+
return beta * (c2 - c1) / N
|
|
97
|
+
|
|
98
|
+
# Check bracket validity
|
|
99
|
+
f_lo = dxcrit(beta_lo) - dx_target
|
|
100
|
+
f_hi = dxcrit(beta_hi) - dx_target
|
|
101
|
+
|
|
102
|
+
if f_lo > 0:
|
|
103
|
+
# dx_target is smaller than achievable with maximum concentration
|
|
104
|
+
return beta_lo
|
|
105
|
+
if f_hi < 0:
|
|
106
|
+
# dx_target is larger than uniform spacing; use near-uniform
|
|
107
|
+
return beta_hi
|
|
108
|
+
|
|
109
|
+
# Bisection search
|
|
110
|
+
lo, hi = beta_lo, beta_hi
|
|
111
|
+
for _ in range(iters):
|
|
112
|
+
mid = 0.5 * (lo + hi)
|
|
113
|
+
f_mid = dxcrit(mid) - dx_target
|
|
114
|
+
if f_mid >= 0:
|
|
115
|
+
hi = mid
|
|
116
|
+
else:
|
|
117
|
+
lo = mid
|
|
118
|
+
|
|
119
|
+
return 0.5 * (lo + hi)
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def _compute_dx_at_crit(
|
|
123
|
+
x_min: float, x_max: float, x_crit: float, num_points: int, beta: float
|
|
124
|
+
) -> float:
|
|
125
|
+
"""
|
|
126
|
+
Compute the local grid spacing at the critical point.
|
|
127
|
+
|
|
128
|
+
This is useful for verifying that the grid achieves the target spacing.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
x_min: Minimum log-price
|
|
132
|
+
x_max: Maximum log-price
|
|
133
|
+
x_crit: Critical log-price
|
|
134
|
+
num_points: Number of grid points
|
|
135
|
+
beta: Concentration parameter
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Local spacing dx at the critical point
|
|
139
|
+
"""
|
|
140
|
+
N = num_points - 1
|
|
141
|
+
c1 = np.arcsinh((x_min - x_crit) / beta)
|
|
142
|
+
c2 = np.arcsinh((x_max - x_crit) / beta)
|
|
143
|
+
return beta * (c2 - c1) / N
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def check_grid_quality(
|
|
147
|
+
dx_vec: np.ndarray, max_ratio: float = DEFAULT_MAX_DX_RATIO
|
|
148
|
+
) -> Tuple[bool, float]:
|
|
149
|
+
"""
|
|
150
|
+
Check grid quality by examining the tail coarseness ratio.
|
|
151
|
+
|
|
152
|
+
A very high ratio of max/min spacing indicates extreme concentration
|
|
153
|
+
that may cause numerical issues in the tails.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
dx_vec: Array of grid spacings
|
|
157
|
+
max_ratio: Maximum acceptable ratio of max/min spacing
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Tuple of (is_acceptable, actual_ratio)
|
|
161
|
+
"""
|
|
162
|
+
if len(dx_vec) == 0:
|
|
163
|
+
return True, 1.0
|
|
164
|
+
|
|
165
|
+
dx_min = np.min(dx_vec)
|
|
166
|
+
dx_max = np.max(dx_vec)
|
|
167
|
+
|
|
168
|
+
if dx_min <= 0:
|
|
169
|
+
return False, np.inf
|
|
170
|
+
|
|
171
|
+
ratio = dx_max / dx_min
|
|
172
|
+
return ratio <= max_ratio, ratio
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def build_uniform_log(
|
|
176
|
+
s_min: float, s_max: float, num_points: int
|
|
177
|
+
) -> Tuple[np.ndarray, np.ndarray, float]:
|
|
178
|
+
"""
|
|
179
|
+
Build a uniform grid in log-price space.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
s_min: Minimum spot price (S space)
|
|
183
|
+
s_max: Maximum spot price (S space)
|
|
184
|
+
num_points: Number of grid points
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Tuple of (x_vec, s_vec, dx):
|
|
188
|
+
- x_vec: Log-price grid points, shape (num_points,)
|
|
189
|
+
- s_vec: Price grid points (exp(x_vec)), shape (num_points,)
|
|
190
|
+
- dx: Uniform grid spacing in log-space
|
|
191
|
+
"""
|
|
192
|
+
if s_min <= 0:
|
|
193
|
+
raise ValueError(f"s_min must be positive, got {s_min}")
|
|
194
|
+
if s_max <= s_min:
|
|
195
|
+
raise ValueError(f"s_max ({s_max}) must be greater than s_min ({s_min})")
|
|
196
|
+
if num_points < 3:
|
|
197
|
+
raise ValueError(f"num_points must be at least 3, got {num_points}")
|
|
198
|
+
|
|
199
|
+
x_min = np.log(s_min)
|
|
200
|
+
x_max = np.log(s_max)
|
|
201
|
+
|
|
202
|
+
x_vec = np.linspace(x_min, x_max, num_points)
|
|
203
|
+
s_vec = np.exp(x_vec)
|
|
204
|
+
dx = (x_max - x_min) / (num_points - 1)
|
|
205
|
+
|
|
206
|
+
return x_vec, s_vec, dx
|
|
207
|
+
|
|
208
|
+
@staticmethod
|
|
209
|
+
def build_tavella_randall(
|
|
210
|
+
s_min: float,
|
|
211
|
+
s_max: float,
|
|
212
|
+
num_points: int,
|
|
213
|
+
critical_point: float,
|
|
214
|
+
beta: Optional[float] = None,
|
|
215
|
+
eps_crit: Optional[float] = None,
|
|
216
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
217
|
+
"""
|
|
218
|
+
Build a non-uniform grid using Tavella-Randall transformation.
|
|
219
|
+
|
|
220
|
+
The transformation concentrates grid points near a critical price
|
|
221
|
+
(e.g., strike or barrier) while maintaining smooth transitions.
|
|
222
|
+
|
|
223
|
+
The transformation is:
|
|
224
|
+
x(w) = x_crit + beta * sinh(c1*(1-w) + c2*w), w = i/N
|
|
225
|
+
|
|
226
|
+
where:
|
|
227
|
+
c1 = arcsinh((x_min - x_crit) / beta)
|
|
228
|
+
c2 = arcsinh((x_max - x_crit) / beta)
|
|
229
|
+
|
|
230
|
+
Beta controls concentration strength (smaller = more concentrated).
|
|
231
|
+
When beta is None, it is automatically calculated to achieve a target
|
|
232
|
+
relative spacing eps_crit near the critical point.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
s_min: Minimum spot price (S space)
|
|
236
|
+
s_max: Maximum spot price (S space)
|
|
237
|
+
num_points: Number of grid points
|
|
238
|
+
critical_point: Price to concentrate around (e.g., strike)
|
|
239
|
+
beta: Concentration parameter. If None, auto-calculated from eps_crit.
|
|
240
|
+
eps_crit: Target relative spacing near critical point (e.g., 0.003 for 0.3%).
|
|
241
|
+
Only used when beta is None. Default: 0.003 (0.3%).
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Tuple of (x_vec, s_vec, dx_vec):
|
|
245
|
+
- x_vec: Log-price grid points
|
|
246
|
+
- s_vec: Price grid points
|
|
247
|
+
- dx_vec: Variable grid spacings
|
|
248
|
+
|
|
249
|
+
Notes:
|
|
250
|
+
The local spacing near the critical point is approximately:
|
|
251
|
+
dx_crit ≈ beta * (c2 - c1) / N
|
|
252
|
+
|
|
253
|
+
In spot space, this corresponds to:
|
|
254
|
+
dS/S ≈ dx_crit ≈ eps_crit
|
|
255
|
+
|
|
256
|
+
Typical values for eps_crit:
|
|
257
|
+
- Pricing only: 0.005 (0.5%)
|
|
258
|
+
- Pricing + Greeks: 0.002-0.003 (0.2-0.3%)
|
|
259
|
+
"""
|
|
260
|
+
if s_min <= 0:
|
|
261
|
+
raise ValueError(f"s_min must be positive, got {s_min}")
|
|
262
|
+
if s_max <= s_min:
|
|
263
|
+
raise ValueError(f"s_max ({s_max}) must be greater than s_min ({s_min})")
|
|
264
|
+
if num_points < 3:
|
|
265
|
+
raise ValueError(f"num_points must be at least 3, got {num_points}")
|
|
266
|
+
if critical_point < s_min or critical_point > s_max:
|
|
267
|
+
raise ValueError(
|
|
268
|
+
f"critical_point ({critical_point}) must be in [{s_min}, {s_max}]"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Convert to log space
|
|
272
|
+
x_min = np.log(s_min)
|
|
273
|
+
x_max = np.log(s_max)
|
|
274
|
+
x_crit = np.log(critical_point)
|
|
275
|
+
|
|
276
|
+
# Auto-calculate beta if not provided
|
|
277
|
+
# Beta is solved via bisection to achieve target local spacing
|
|
278
|
+
if beta is None:
|
|
279
|
+
if eps_crit is None:
|
|
280
|
+
eps_crit = SpatialGrid.DEFAULT_EPSILON_CRIT
|
|
281
|
+
|
|
282
|
+
# Target spacing in log-space: ln(1 + eps) ≈ eps for small eps
|
|
283
|
+
dx_target = np.log1p(eps_crit)
|
|
284
|
+
|
|
285
|
+
beta = SpatialGrid._beta_for_target_dx(
|
|
286
|
+
x_min, x_max, x_crit, num_points, dx_target
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Calculate Tavella-Randall transformation parameters
|
|
290
|
+
c1 = np.arcsinh((x_min - x_crit) / beta)
|
|
291
|
+
c2 = np.arcsinh((x_max - x_crit) / beta)
|
|
292
|
+
|
|
293
|
+
# Generate grid
|
|
294
|
+
N = num_points - 1
|
|
295
|
+
i_vec = np.arange(num_points)
|
|
296
|
+
|
|
297
|
+
# Transformation: x_i = x_crit + beta * sinh(c1 * (1 - i/N) + c2 * (i/N))
|
|
298
|
+
weight = i_vec / N
|
|
299
|
+
x_vec = x_crit + beta * np.sinh(c1 * (1.0 - weight) + c2 * weight)
|
|
300
|
+
|
|
301
|
+
# Convert back to price space
|
|
302
|
+
s_vec = np.exp(x_vec)
|
|
303
|
+
|
|
304
|
+
# Calculate variable grid spacings
|
|
305
|
+
dx_vec = np.diff(x_vec)
|
|
306
|
+
|
|
307
|
+
return x_vec, s_vec, dx_vec
|
|
308
|
+
|
|
309
|
+
# ============================================================
|
|
310
|
+
# ODE-based Tavella-Randall helper methods for multiple critical points
|
|
311
|
+
# ============================================================
|
|
312
|
+
|
|
313
|
+
@staticmethod
|
|
314
|
+
def _ode_f(y: float, A: float, beta: float, crits: np.ndarray) -> float:
|
|
315
|
+
"""
|
|
316
|
+
Right-hand side of the ODE for multi-critical-point grid generation.
|
|
317
|
+
|
|
318
|
+
ODE: dY/de = A * (sum_k J_k^-2)^(-0.5)
|
|
319
|
+
where J_k = sqrt(beta^2 + (Y - B_k)^2)
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
y: Current position in log-space
|
|
323
|
+
A: Scaling constant (found via shooting)
|
|
324
|
+
beta: Concentration parameter
|
|
325
|
+
crits: Array of critical points in log-space
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
dY/de value at current position
|
|
329
|
+
"""
|
|
330
|
+
j_sq = beta * beta + (y - crits) ** 2
|
|
331
|
+
s = np.sum(1.0 / j_sq)
|
|
332
|
+
return A / np.sqrt(s)
|
|
333
|
+
|
|
334
|
+
@staticmethod
|
|
335
|
+
def _ode_rk4_step(
|
|
336
|
+
y: float, h: float, A: float, beta: float, crits: np.ndarray
|
|
337
|
+
) -> float:
|
|
338
|
+
"""
|
|
339
|
+
Single RK4 (Runge-Kutta 4th order) integration step.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
y: Current position
|
|
343
|
+
h: Step size in parameter space e
|
|
344
|
+
A: Scaling constant
|
|
345
|
+
beta: Concentration parameter
|
|
346
|
+
crits: Critical points array
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
New position after one RK4 step
|
|
350
|
+
"""
|
|
351
|
+
f = SpatialGrid._ode_f
|
|
352
|
+
k1 = f(y, A, beta, crits)
|
|
353
|
+
k2 = f(y + 0.5 * h * k1, A, beta, crits)
|
|
354
|
+
k3 = f(y + 0.5 * h * k2, A, beta, crits)
|
|
355
|
+
k4 = f(y + h * k3, A, beta, crits)
|
|
356
|
+
return y + (h / 6.0) * (k1 + 2.0 * k2 + 2.0 * k3 + k4)
|
|
357
|
+
|
|
358
|
+
@staticmethod
|
|
359
|
+
def _ode_integrate(
|
|
360
|
+
y_min: float, N: int, A: float, beta: float, crits: np.ndarray
|
|
361
|
+
) -> np.ndarray:
|
|
362
|
+
"""
|
|
363
|
+
Integrate ODE from e=0 to e=1 with N steps.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
y_min: Starting position (Y(0) = y_min)
|
|
367
|
+
N: Number of steps (generates N+1 points)
|
|
368
|
+
A: Scaling constant
|
|
369
|
+
beta: Concentration parameter
|
|
370
|
+
crits: Critical points array
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
Array of N+1 grid points
|
|
374
|
+
"""
|
|
375
|
+
h = 1.0 / N
|
|
376
|
+
mesh = np.empty(N + 1, dtype=float)
|
|
377
|
+
mesh[0] = y_min
|
|
378
|
+
y = y_min
|
|
379
|
+
for i in range(1, N + 1):
|
|
380
|
+
y = SpatialGrid._ode_rk4_step(y, h, A, beta, crits)
|
|
381
|
+
mesh[i] = y
|
|
382
|
+
return mesh
|
|
383
|
+
|
|
384
|
+
@staticmethod
|
|
385
|
+
def _ode_find_A(
|
|
386
|
+
y_min: float, y_max: float, N: int, beta: float, crits: np.ndarray
|
|
387
|
+
) -> float:
|
|
388
|
+
"""
|
|
389
|
+
Find scaling constant A via shooting method (bisection).
|
|
390
|
+
|
|
391
|
+
Solves for A such that Y(1) = y_max given Y(0) = y_min.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
y_min: Lower boundary
|
|
395
|
+
y_max: Upper boundary
|
|
396
|
+
N: Number of grid intervals
|
|
397
|
+
beta: Concentration parameter
|
|
398
|
+
crits: Critical points array
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Scaling constant A
|
|
402
|
+
"""
|
|
403
|
+
a_lo = 0.0
|
|
404
|
+
a_hi = max(4.0 * abs(y_max), abs(y_max - y_min))
|
|
405
|
+
|
|
406
|
+
def residual(A: float) -> float:
|
|
407
|
+
mesh = SpatialGrid._ode_integrate(y_min, N, A, beta, crits)
|
|
408
|
+
return mesh[-1] - y_max
|
|
409
|
+
|
|
410
|
+
f_lo = residual(a_lo)
|
|
411
|
+
f_hi = residual(a_hi)
|
|
412
|
+
|
|
413
|
+
# Expand bracket if needed
|
|
414
|
+
for _ in range(20):
|
|
415
|
+
if f_lo * f_hi <= 0:
|
|
416
|
+
break
|
|
417
|
+
a_hi *= 2.0
|
|
418
|
+
f_hi = residual(a_hi)
|
|
419
|
+
|
|
420
|
+
# Bisection
|
|
421
|
+
tol = 1e-10
|
|
422
|
+
for _ in range(100):
|
|
423
|
+
if abs(a_hi - a_lo) <= tol:
|
|
424
|
+
break
|
|
425
|
+
a_mid = 0.5 * (a_lo + a_hi)
|
|
426
|
+
f_mid = residual(a_mid)
|
|
427
|
+
if f_mid == 0.0:
|
|
428
|
+
return a_mid
|
|
429
|
+
if f_lo * f_mid < 0:
|
|
430
|
+
a_hi, f_hi = a_mid, f_mid
|
|
431
|
+
else:
|
|
432
|
+
a_lo, f_lo = a_mid, f_mid
|
|
433
|
+
|
|
434
|
+
return 0.5 * (a_lo + a_hi)
|
|
435
|
+
|
|
436
|
+
@staticmethod
|
|
437
|
+
def _calculate_beta_for_multi_crit(
|
|
438
|
+
x_min: float,
|
|
439
|
+
x_max: float,
|
|
440
|
+
x_crits: np.ndarray,
|
|
441
|
+
num_points: int,
|
|
442
|
+
eps_crit: float,
|
|
443
|
+
use_heuristic_beta: bool = False,
|
|
444
|
+
) -> float:
|
|
445
|
+
"""
|
|
446
|
+
Find beta that achieves target spacing at critical points.
|
|
447
|
+
|
|
448
|
+
Uses bisection to find beta such that the minimum spacing
|
|
449
|
+
near any critical point is approximately eps_crit.
|
|
450
|
+
|
|
451
|
+
Optimization: Uses a coarse grid for the expensive search,
|
|
452
|
+
scaling the target spacing accordingly.
|
|
453
|
+
"""
|
|
454
|
+
if use_heuristic_beta:
|
|
455
|
+
M = len(x_crits)
|
|
456
|
+
# Heuristic: beta ~ ln(1+eps) * sqrt(M/12)
|
|
457
|
+
# Adjusts concentration based on number of observation dates
|
|
458
|
+
beta = np.log1p(eps_crit) * np.sqrt(max(M, 1) / 12.0)
|
|
459
|
+
|
|
460
|
+
# Check grid quality and adjust if necessary
|
|
461
|
+
# Ensure spacing ratio is acceptable (avoid tail coarseness)
|
|
462
|
+
N_check = num_points - 1
|
|
463
|
+
max_check_iters = 5
|
|
464
|
+
|
|
465
|
+
for _ in range(max_check_iters):
|
|
466
|
+
# Generate grid to check spacing ratio
|
|
467
|
+
A = SpatialGrid._ode_find_A(x_min, x_max, N_check, beta, x_crits)
|
|
468
|
+
mesh = SpatialGrid._ode_integrate(x_min, N_check, A, beta, x_crits)
|
|
469
|
+
dx = np.diff(mesh)
|
|
470
|
+
|
|
471
|
+
is_acceptable, _ = SpatialGrid.check_grid_quality(dx, max_ratio=100.0)
|
|
472
|
+
if is_acceptable:
|
|
473
|
+
break
|
|
474
|
+
|
|
475
|
+
# If ratio too high, increase beta (smoother grid)
|
|
476
|
+
beta *= 1.2
|
|
477
|
+
|
|
478
|
+
return beta
|
|
479
|
+
|
|
480
|
+
dx_target = np.log1p(eps_crit)
|
|
481
|
+
N = num_points - 1
|
|
482
|
+
|
|
483
|
+
# Use coarse grid for beta search to improve performance
|
|
484
|
+
# Beta is a shape parameter, so we can estimate it on a coarser mesh
|
|
485
|
+
N_coarse = min(N, 64)
|
|
486
|
+
dx_target_adj = dx_target * (N / N_coarse)
|
|
487
|
+
|
|
488
|
+
# Bracket for beta (log-scale search)
|
|
489
|
+
beta_lo = 1e-6 * (x_max - x_min)
|
|
490
|
+
beta_hi = 10.0 * (x_max - x_min)
|
|
491
|
+
|
|
492
|
+
def min_spacing_near_crits(beta: float) -> float:
|
|
493
|
+
A = SpatialGrid._ode_find_A(x_min, x_max, N_coarse, beta, x_crits)
|
|
494
|
+
mesh = SpatialGrid._ode_integrate(x_min, N_coarse, A, beta, x_crits)
|
|
495
|
+
dx = np.diff(mesh)
|
|
496
|
+
|
|
497
|
+
# Find minimum spacing near any critical point
|
|
498
|
+
min_dx = float("inf")
|
|
499
|
+
for xc in x_crits:
|
|
500
|
+
idx = np.searchsorted(mesh, xc)
|
|
501
|
+
idx = max(0, min(idx, len(dx) - 1))
|
|
502
|
+
if idx > 0:
|
|
503
|
+
min_dx = min(min_dx, dx[idx - 1])
|
|
504
|
+
if idx < len(dx):
|
|
505
|
+
min_dx = min(min_dx, dx[idx])
|
|
506
|
+
return min_dx
|
|
507
|
+
|
|
508
|
+
# Bisection with geometric mean (log-scale)
|
|
509
|
+
# Reduced iterations (20) as precise beta is not critical for grid quality
|
|
510
|
+
for _ in range(20):
|
|
511
|
+
beta_mid = np.sqrt(beta_lo * beta_hi)
|
|
512
|
+
current_dx = min_spacing_near_crits(beta_mid)
|
|
513
|
+
if current_dx > dx_target_adj:
|
|
514
|
+
beta_hi = beta_mid # Need tighter concentration
|
|
515
|
+
else:
|
|
516
|
+
beta_lo = beta_mid # Loosen concentration
|
|
517
|
+
|
|
518
|
+
return np.sqrt(beta_lo * beta_hi)
|
|
519
|
+
|
|
520
|
+
@staticmethod
|
|
521
|
+
def _snap_critical_points(x_vec: np.ndarray, x_crits: np.ndarray) -> np.ndarray:
|
|
522
|
+
"""
|
|
523
|
+
Snap nearest grid points to exact critical values.
|
|
524
|
+
|
|
525
|
+
Ensures critical points are exactly included in the grid,
|
|
526
|
+
which is important for barrier boundary conditions.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
x_vec: Grid points array
|
|
530
|
+
x_crits: Critical points to snap to
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
Grid with critical points exactly included
|
|
534
|
+
"""
|
|
535
|
+
x_vec = x_vec.copy()
|
|
536
|
+
for xc in x_crits:
|
|
537
|
+
# Find nearest grid index
|
|
538
|
+
idx = np.argmin(np.abs(x_vec - xc))
|
|
539
|
+
# Snap to exact critical value
|
|
540
|
+
x_vec[idx] = xc
|
|
541
|
+
# Re-sort to maintain monotonicity (shouldn't change much)
|
|
542
|
+
x_vec = np.sort(x_vec)
|
|
543
|
+
return x_vec
|
|
544
|
+
|
|
545
|
+
@staticmethod
|
|
546
|
+
def build_tavella_randall_multi(
|
|
547
|
+
s_min: float,
|
|
548
|
+
s_max: float,
|
|
549
|
+
num_points: int,
|
|
550
|
+
critical_points: List[float],
|
|
551
|
+
beta: Optional[float] = None,
|
|
552
|
+
eps_crit: Optional[float] = None,
|
|
553
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
554
|
+
"""
|
|
555
|
+
Build a non-uniform grid with concentration near multiple critical points.
|
|
556
|
+
|
|
557
|
+
Uses the ODE-based Tavella-Randall method:
|
|
558
|
+
dY/de = A * (sum_k J_k^-2)^(-0.5)
|
|
559
|
+
where J_k = sqrt(beta^2 + (Y - B_k)^2), solved via RK4 with shooting.
|
|
560
|
+
|
|
561
|
+
This method provides C-infinity smooth grids that naturally concentrate
|
|
562
|
+
around all critical points simultaneously.
|
|
563
|
+
|
|
564
|
+
Args:
|
|
565
|
+
s_min: Minimum spot price (S space)
|
|
566
|
+
s_max: Maximum spot price (S space)
|
|
567
|
+
num_points: Total number of grid points
|
|
568
|
+
critical_points: List of prices to concentrate around
|
|
569
|
+
beta: Concentration parameter. If None, auto-calculated from eps_crit.
|
|
570
|
+
eps_crit: Target relative spacing near critical points (e.g., 0.003 for 0.3%).
|
|
571
|
+
Only used when beta is None. Default: 0.003 (0.3%).
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
Tuple of (x_vec, s_vec, dx_vec):
|
|
575
|
+
- x_vec: Log-price grid points
|
|
576
|
+
- s_vec: Price grid points
|
|
577
|
+
- dx_vec: Variable grid spacings
|
|
578
|
+
|
|
579
|
+
Notes:
|
|
580
|
+
Critical points are exactly included in the final grid via post-processing,
|
|
581
|
+
which is important for barrier boundary conditions.
|
|
582
|
+
"""
|
|
583
|
+
# Validation
|
|
584
|
+
if s_min <= 0:
|
|
585
|
+
raise ValueError(f"s_min must be positive, got {s_min}")
|
|
586
|
+
if s_max <= s_min:
|
|
587
|
+
raise ValueError(f"s_max ({s_max}) must be greater than s_min ({s_min})")
|
|
588
|
+
if num_points < 3:
|
|
589
|
+
raise ValueError(f"num_points must be at least 3, got {num_points}")
|
|
590
|
+
|
|
591
|
+
# Handle empty critical points -> uniform grid
|
|
592
|
+
if not critical_points:
|
|
593
|
+
x_vec, s_vec, dx = SpatialGrid.build_uniform_log(s_min, s_max, num_points)
|
|
594
|
+
dx_vec = np.full(num_points - 1, dx)
|
|
595
|
+
return x_vec, s_vec, dx_vec
|
|
596
|
+
|
|
597
|
+
# Filter and sort critical points within bounds
|
|
598
|
+
crits = np.array([c for c in critical_points if s_min < c < s_max])
|
|
599
|
+
if len(crits) == 0:
|
|
600
|
+
# No valid critical points, use uniform grid
|
|
601
|
+
x_vec, s_vec, dx = SpatialGrid.build_uniform_log(s_min, s_max, num_points)
|
|
602
|
+
dx_vec = np.full(num_points - 1, dx)
|
|
603
|
+
return x_vec, s_vec, dx_vec
|
|
604
|
+
|
|
605
|
+
crits = np.sort(np.unique(crits))
|
|
606
|
+
|
|
607
|
+
# If only one critical point, use single Tavella-Randall (optimization)
|
|
608
|
+
if len(crits) == 1:
|
|
609
|
+
return SpatialGrid.build_tavella_randall(
|
|
610
|
+
s_min, s_max, num_points, crits[0], beta, eps_crit
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
# Convert to log-space
|
|
614
|
+
x_min = np.log(s_min)
|
|
615
|
+
x_max = np.log(s_max)
|
|
616
|
+
x_crits = np.log(crits)
|
|
617
|
+
N = num_points - 1
|
|
618
|
+
|
|
619
|
+
# Auto-calculate beta if not provided
|
|
620
|
+
if beta is None:
|
|
621
|
+
if eps_crit is None:
|
|
622
|
+
eps_crit = SpatialGrid.DEFAULT_EPSILON_CRIT
|
|
623
|
+
beta = SpatialGrid._calculate_beta_for_multi_crit(
|
|
624
|
+
x_min, x_max, x_crits, num_points, eps_crit
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
# Safeguard against extreme concentration
|
|
628
|
+
beta = max(beta, 1e-10 * (x_max - x_min))
|
|
629
|
+
|
|
630
|
+
# Find scaling constant A via shooting method
|
|
631
|
+
A = SpatialGrid._ode_find_A(x_min, x_max, N, beta, x_crits)
|
|
632
|
+
|
|
633
|
+
# Generate mesh via RK4 integration
|
|
634
|
+
x_vec = SpatialGrid._ode_integrate(x_min, N, A, beta, x_crits)
|
|
635
|
+
|
|
636
|
+
# Snap critical points to grid (ensure exact inclusion)
|
|
637
|
+
x_vec = SpatialGrid._snap_critical_points(x_vec, x_crits)
|
|
638
|
+
|
|
639
|
+
# Convert to price space and compute spacings
|
|
640
|
+
s_vec = np.exp(x_vec)
|
|
641
|
+
dx_vec = np.diff(x_vec)
|
|
642
|
+
|
|
643
|
+
return x_vec, s_vec, dx_vec
|
|
644
|
+
|
|
645
|
+
@staticmethod
|
|
646
|
+
def calculate_auto_bounds(
|
|
647
|
+
spot: float,
|
|
648
|
+
sigma: float,
|
|
649
|
+
tau: float,
|
|
650
|
+
r: float = 0.0,
|
|
651
|
+
q: float = 0.0,
|
|
652
|
+
strike: Optional[float] = None,
|
|
653
|
+
barriers: Optional[List[float]] = None,
|
|
654
|
+
num_std: float = 4.0,
|
|
655
|
+
) -> Tuple[float, float]:
|
|
656
|
+
"""
|
|
657
|
+
Automatically calculate appropriate grid bounds.
|
|
658
|
+
|
|
659
|
+
Uses volatility-based expansion to ensure the grid captures
|
|
660
|
+
the relevant price range for the option.
|
|
661
|
+
|
|
662
|
+
Args:
|
|
663
|
+
spot: Current spot price
|
|
664
|
+
sigma: Volatility
|
|
665
|
+
tau: Time to maturity in years
|
|
666
|
+
r: Risk-free rate (default: 0)
|
|
667
|
+
q: Dividend yield (default: 0)
|
|
668
|
+
strike: Strike price (optional, ensures it's in bounds)
|
|
669
|
+
barriers: Barrier prices (optional, ensures they're in bounds)
|
|
670
|
+
num_std: Number of standard deviations for bounds (default: 4)
|
|
671
|
+
|
|
672
|
+
Returns:
|
|
673
|
+
Tuple of (s_min, s_max)
|
|
674
|
+
"""
|
|
675
|
+
if spot <= 0:
|
|
676
|
+
raise ValueError(f"spot must be positive, got {spot}")
|
|
677
|
+
if sigma <= 0:
|
|
678
|
+
raise ValueError(f"sigma must be positive, got {sigma}")
|
|
679
|
+
if tau <= 0:
|
|
680
|
+
raise ValueError(f"tau must be positive, got {tau}")
|
|
681
|
+
|
|
682
|
+
# Calculate expected drift and volatility range
|
|
683
|
+
drift = (r - q) * tau
|
|
684
|
+
vol_range = num_std * sigma * np.sqrt(tau)
|
|
685
|
+
|
|
686
|
+
# Calculate bounds
|
|
687
|
+
s_min = spot * np.exp(drift - vol_range)
|
|
688
|
+
s_max = spot * np.exp(drift + vol_range)
|
|
689
|
+
|
|
690
|
+
# Ensure strike is within bounds
|
|
691
|
+
if strike is not None and strike > 0:
|
|
692
|
+
s_min = min(s_min, strike * 0.5)
|
|
693
|
+
s_max = max(s_max, strike * 2.0)
|
|
694
|
+
|
|
695
|
+
# Ensure barriers are within bounds
|
|
696
|
+
if barriers is not None:
|
|
697
|
+
for b in barriers:
|
|
698
|
+
if b > 0:
|
|
699
|
+
s_min = min(s_min, b * 0.8)
|
|
700
|
+
s_max = max(s_max, b * 1.2)
|
|
701
|
+
|
|
702
|
+
# Sanity bounds
|
|
703
|
+
s_min = max(s_min, spot * 0.01) # At least 1% of spot
|
|
704
|
+
s_max = min(s_max, spot * 100.0) # At most 100x spot
|
|
705
|
+
|
|
706
|
+
return s_min, s_max
|
|
707
|
+
|
|
708
|
+
@staticmethod
|
|
709
|
+
def build(
|
|
710
|
+
s_min: float,
|
|
711
|
+
s_max: float,
|
|
712
|
+
num_points: int,
|
|
713
|
+
critical_points: Optional[List[float]] = None,
|
|
714
|
+
use_adaptive: bool = False,
|
|
715
|
+
beta: Optional[float] = None,
|
|
716
|
+
eps_crit: Optional[float] = None,
|
|
717
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
718
|
+
"""
|
|
719
|
+
Build a spatial grid using the appropriate method.
|
|
720
|
+
|
|
721
|
+
This is a convenience method that chooses between uniform and
|
|
722
|
+
Tavella-Randall based on the parameters.
|
|
723
|
+
|
|
724
|
+
Args:
|
|
725
|
+
s_min: Minimum spot price
|
|
726
|
+
s_max: Maximum spot price
|
|
727
|
+
num_points: Number of grid points
|
|
728
|
+
critical_points: Points to concentrate around (optional)
|
|
729
|
+
use_adaptive: Whether to use Tavella-Randall transformation
|
|
730
|
+
beta: Concentration parameter for Tavella-Randall. If None, auto-calculated.
|
|
731
|
+
eps_crit: Target relative spacing near critical points (e.g., 0.003 for 0.3%).
|
|
732
|
+
Only used when beta is None and use_adaptive is True.
|
|
733
|
+
|
|
734
|
+
Returns:
|
|
735
|
+
Tuple of (x_vec, s_vec, dx_vec)
|
|
736
|
+
"""
|
|
737
|
+
if use_adaptive and critical_points:
|
|
738
|
+
return SpatialGrid.build_tavella_randall_multi(
|
|
739
|
+
s_min, s_max, num_points, critical_points, beta, eps_crit
|
|
740
|
+
)
|
|
741
|
+
elif use_adaptive and not critical_points:
|
|
742
|
+
# Use midpoint as critical point
|
|
743
|
+
mid = np.sqrt(s_min * s_max) # Geometric mean
|
|
744
|
+
return SpatialGrid.build_tavella_randall(
|
|
745
|
+
s_min, s_max, num_points, mid, beta, eps_crit
|
|
746
|
+
)
|
|
747
|
+
else:
|
|
748
|
+
x_vec, s_vec, dx = SpatialGrid.build_uniform_log(s_min, s_max, num_points)
|
|
749
|
+
dx_vec = np.full(num_points - 1, dx)
|
|
750
|
+
return x_vec, s_vec, dx_vec
|