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,163 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Created on Mon Nov 17 2025
|
|
3
|
+
|
|
4
|
+
@description: RQMC batching and adaptive stopping driver for Monte Carlo
|
|
5
|
+
pricing engines using GBMPathGenerator or similar path
|
|
6
|
+
generators.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Callable, Dict, Optional, Protocol, Tuple
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PathGenerator(Protocol):
|
|
18
|
+
"""
|
|
19
|
+
Minimal protocol for path generators compatible with the RQMC driver.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
num_paths: int
|
|
23
|
+
|
|
24
|
+
def generate_paths(
|
|
25
|
+
self,
|
|
26
|
+
seed: Optional[int] = None,
|
|
27
|
+
batch_id: Optional[int] = None,
|
|
28
|
+
return_aux: bool = False,
|
|
29
|
+
) -> Tuple[np.ndarray, Optional[Dict[str, np.ndarray]]]:
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
PricerFn = Callable[[np.ndarray, Optional[Dict[str, np.ndarray]]], np.ndarray]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class RQMCResult:
|
|
38
|
+
"""
|
|
39
|
+
Result container for RQMC batching runs.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
price: float
|
|
43
|
+
std_error: float
|
|
44
|
+
total_paths: int
|
|
45
|
+
batches_used: int
|
|
46
|
+
batch_means: np.ndarray
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def run_rqmc(
|
|
50
|
+
pricer_fn: PricerFn,
|
|
51
|
+
path_generator: PathGenerator,
|
|
52
|
+
max_batches: int,
|
|
53
|
+
target_std: float,
|
|
54
|
+
min_batches: int = 1,
|
|
55
|
+
) -> RQMCResult:
|
|
56
|
+
"""
|
|
57
|
+
Run randomized QMC (RQMC) in batches with adaptive stopping.
|
|
58
|
+
|
|
59
|
+
Each batch corresponds to an independent scrambled Sobol sequence (or
|
|
60
|
+
independent pseudorandom run), and the estimator is the average of the
|
|
61
|
+
batch means. The standard error is based on the sample variance of the
|
|
62
|
+
batch means.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
pricer_fn : callable
|
|
67
|
+
Function with signature pricer_fn(paths, aux) -> payoffs_per_path,
|
|
68
|
+
where paths is an array of shape (n_paths, n_steps + 1) and aux is
|
|
69
|
+
a dictionary with auxiliary data (e.g., importance weights).
|
|
70
|
+
path_generator : PathGenerator
|
|
71
|
+
Path generator implementing the PathGenerator protocol.
|
|
72
|
+
max_batches : int
|
|
73
|
+
Maximum number of RQMC batches to run.
|
|
74
|
+
target_std : float
|
|
75
|
+
Target standard error for stopping the simulation.
|
|
76
|
+
min_batches : int, optional
|
|
77
|
+
Minimum number of batches to run before checking the stopping
|
|
78
|
+
criterion.
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
RQMCResult
|
|
83
|
+
Result object containing the estimated price, standard error,
|
|
84
|
+
total number of paths used, and per-batch means.
|
|
85
|
+
"""
|
|
86
|
+
if max_batches <= 0:
|
|
87
|
+
raise ValueError("max_batches must be positive")
|
|
88
|
+
if min_batches <= 0:
|
|
89
|
+
raise ValueError("min_batches must be positive")
|
|
90
|
+
if min_batches > max_batches:
|
|
91
|
+
raise ValueError("min_batches cannot exceed max_batches")
|
|
92
|
+
if target_std <= 0.0:
|
|
93
|
+
raise ValueError("target_std must be positive")
|
|
94
|
+
|
|
95
|
+
batch_means = []
|
|
96
|
+
n_paths_per_batch = path_generator.num_paths
|
|
97
|
+
|
|
98
|
+
# Welford's algorithm over batch means
|
|
99
|
+
mean = 0.0
|
|
100
|
+
m2 = 0.0
|
|
101
|
+
|
|
102
|
+
for batch_id in range(max_batches):
|
|
103
|
+
paths, aux = path_generator.generate_paths(batch_id=batch_id, return_aux=True)
|
|
104
|
+
payoffs = pricer_fn(paths, aux)
|
|
105
|
+
payoffs = np.asarray(payoffs, dtype=float)
|
|
106
|
+
if payoffs.ndim != 1 or payoffs.shape[0] != n_paths_per_batch:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
"pricer_fn must return a 1D array with one payoff per path "
|
|
109
|
+
f"(expected length {n_paths_per_batch}, got {payoffs.shape})."
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
batch_mean = float(payoffs.mean())
|
|
113
|
+
batch_means.append(batch_mean)
|
|
114
|
+
|
|
115
|
+
n = batch_id + 1
|
|
116
|
+
delta = batch_mean - mean
|
|
117
|
+
mean += delta / n
|
|
118
|
+
m2 += delta * (batch_mean - mean)
|
|
119
|
+
|
|
120
|
+
if n >= min_batches:
|
|
121
|
+
if n > 1:
|
|
122
|
+
variance = m2 / (n - 1)
|
|
123
|
+
else:
|
|
124
|
+
variance = 0.0
|
|
125
|
+
std_error = np.sqrt(variance / n)
|
|
126
|
+
|
|
127
|
+
if std_error <= target_std or n == max_batches:
|
|
128
|
+
return RQMCResult(
|
|
129
|
+
price=mean,
|
|
130
|
+
std_error=std_error,
|
|
131
|
+
total_paths=n * n_paths_per_batch,
|
|
132
|
+
batches_used=n,
|
|
133
|
+
batch_means=np.array(batch_means, dtype=float),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Fallback (should not normally reach here)
|
|
137
|
+
if len(batch_means) == 0:
|
|
138
|
+
raise RuntimeError("No batches were run in RQMC driver.")
|
|
139
|
+
|
|
140
|
+
n = len(batch_means)
|
|
141
|
+
batch_means_arr = np.array(batch_means, dtype=float)
|
|
142
|
+
mean = float(batch_means_arr.mean())
|
|
143
|
+
if n > 1:
|
|
144
|
+
variance = float(batch_means_arr.var(ddof=1))
|
|
145
|
+
else:
|
|
146
|
+
variance = 0.0
|
|
147
|
+
std_error = np.sqrt(variance / n)
|
|
148
|
+
|
|
149
|
+
return RQMCResult(
|
|
150
|
+
price=mean,
|
|
151
|
+
std_error=std_error,
|
|
152
|
+
total_paths=n * n_paths_per_batch,
|
|
153
|
+
batches_used=n,
|
|
154
|
+
batch_means=batch_means_arr,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
__all__ = [
|
|
159
|
+
"RQMCResult",
|
|
160
|
+
"run_rqmc",
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Created on Mon Nov 17 2025
|
|
3
|
+
|
|
4
|
+
@author: yaofuxin
|
|
5
|
+
@description: Sobol-based and pseudorandom normal generators for MC/QMC path
|
|
6
|
+
construction, with support for RQMC batching.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Optional, Protocol
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from scipy import special
|
|
18
|
+
from scipy.stats import qmc
|
|
19
|
+
|
|
20
|
+
HAS_SCIPY_QMC = True
|
|
21
|
+
except ImportError: # pragma: no cover - fallback path without SciPy
|
|
22
|
+
HAS_SCIPY_QMC = False
|
|
23
|
+
special = None # type: ignore
|
|
24
|
+
qmc = None # type: ignore
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RandomStream(Protocol):
|
|
28
|
+
"""
|
|
29
|
+
Minimal interface for random streams used by path generators.
|
|
30
|
+
|
|
31
|
+
Implementations should return standard normal random numbers with shape
|
|
32
|
+
(n_paths, dim). The optional batch_id is used to generate independent
|
|
33
|
+
randomized QMC batches.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def normal(
|
|
37
|
+
self, n_paths: int, dim: int, batch_id: Optional[int] = None
|
|
38
|
+
) -> np.ndarray:
|
|
39
|
+
"""
|
|
40
|
+
Generate standard normal random numbers.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
n_paths : int
|
|
45
|
+
Number of Monte Carlo paths.
|
|
46
|
+
dim : int
|
|
47
|
+
Dimension of each path (typically number of time steps).
|
|
48
|
+
batch_id : int, optional
|
|
49
|
+
Identifier for the RQMC batch. Implementations can use this to
|
|
50
|
+
produce independent randomized batches.
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
np.ndarray
|
|
55
|
+
Array of shape (n_paths, dim) with N(0, 1) samples.
|
|
56
|
+
"""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class PseudoRandomNormalGenerator:
|
|
62
|
+
"""
|
|
63
|
+
Pseudorandom standard normal generator based on NumPy.
|
|
64
|
+
|
|
65
|
+
This implements the RandomStream protocol and is suitable for classical
|
|
66
|
+
Monte Carlo simulations.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
seed: Optional[int] = None
|
|
70
|
+
|
|
71
|
+
def __post_init__(self) -> None:
|
|
72
|
+
self._rng = np.random.default_rng(self.seed)
|
|
73
|
+
|
|
74
|
+
def normal(
|
|
75
|
+
self, n_paths: int, dim: int, batch_id: Optional[int] = None
|
|
76
|
+
) -> np.ndarray:
|
|
77
|
+
"""
|
|
78
|
+
Generate standard normal samples using NumPy's Generator.
|
|
79
|
+
|
|
80
|
+
The batch_id argument is accepted for API compatibility but ignored,
|
|
81
|
+
since independent batches are handled via the RNG state.
|
|
82
|
+
"""
|
|
83
|
+
return self._rng.standard_normal(size=(n_paths, dim))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _next_power_of_two(n: int) -> int:
|
|
87
|
+
"""Return the smallest power of two >= n."""
|
|
88
|
+
if n <= 1:
|
|
89
|
+
return 1
|
|
90
|
+
return 1 << (n - 1).bit_length()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class SobolNormalGenerator:
|
|
95
|
+
"""
|
|
96
|
+
Sobol-based standard normal generator with optional RQMC batching.
|
|
97
|
+
|
|
98
|
+
This class wraps scipy.stats.qmc.Sobol and uses scipy.special.ndtri to
|
|
99
|
+
transform low-discrepancy uniform samples into standard normals. To
|
|
100
|
+
preserve Sobol structure, the number of generated samples is always a
|
|
101
|
+
power of two (2**m). If the requested path count is not a power of two,
|
|
102
|
+
the generator will either round up and truncate or raise an error,
|
|
103
|
+
depending on the configuration.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
base_seed: int = 1234
|
|
107
|
+
strict_power_of_two: bool = False
|
|
108
|
+
|
|
109
|
+
def _check_scipy(self) -> None:
|
|
110
|
+
if not HAS_SCIPY_QMC:
|
|
111
|
+
raise ImportError(
|
|
112
|
+
"SobolNormalGenerator requires scipy.stats.qmc and scipy.special.ndtri. "
|
|
113
|
+
"Please install SciPy or use PseudoRandomNormalGenerator instead."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def _make_engine(self, dim: int, batch_id: Optional[int]) -> "qmc.Sobol":
|
|
117
|
+
"""
|
|
118
|
+
Create a new Sobol engine for the given dimension and batch.
|
|
119
|
+
|
|
120
|
+
A different seed is used for each batch_id to obtain independent
|
|
121
|
+
scrambled Sobol sequences (RQMC).
|
|
122
|
+
"""
|
|
123
|
+
self._check_scipy()
|
|
124
|
+
# Use different seeds for different batches to obtain independent scrambles
|
|
125
|
+
if batch_id is None:
|
|
126
|
+
seed = self.base_seed
|
|
127
|
+
else:
|
|
128
|
+
seed = self.base_seed + int(batch_id)
|
|
129
|
+
return qmc.Sobol(d=dim, scramble=True, seed=seed) # type: ignore[call-arg]
|
|
130
|
+
|
|
131
|
+
def normal(
|
|
132
|
+
self, n_paths: int, dim: int, batch_id: Optional[int] = None
|
|
133
|
+
) -> np.ndarray:
|
|
134
|
+
"""
|
|
135
|
+
Generate standard normal samples using a scrambled Sobol sequence.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
n_paths : int
|
|
140
|
+
Requested number of paths.
|
|
141
|
+
dim : int
|
|
142
|
+
Dimension of each path (typically number of time steps).
|
|
143
|
+
batch_id : int, optional
|
|
144
|
+
Batch identifier for RQMC. Different batch_ids produce independent
|
|
145
|
+
randomized Sobol sequences.
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
np.ndarray
|
|
150
|
+
Array of shape (n_paths, dim) containing N(0, 1) samples.
|
|
151
|
+
"""
|
|
152
|
+
if n_paths <= 0:
|
|
153
|
+
raise ValueError("n_paths must be positive")
|
|
154
|
+
if dim <= 0:
|
|
155
|
+
raise ValueError("dim must be positive")
|
|
156
|
+
|
|
157
|
+
self._check_scipy()
|
|
158
|
+
|
|
159
|
+
# Ensure we use exactly 2**m Sobol points to preserve balance properties
|
|
160
|
+
n_total = _next_power_of_two(n_paths)
|
|
161
|
+
if self.strict_power_of_two and n_total != n_paths:
|
|
162
|
+
raise ValueError(
|
|
163
|
+
f"SobolNormalGenerator with strict_power_of_two=True requires "
|
|
164
|
+
f"n_paths to be a power of two, got {n_paths}."
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
m = int(np.log2(n_total))
|
|
168
|
+
engine = self._make_engine(dim=dim, batch_id=batch_id)
|
|
169
|
+
|
|
170
|
+
# Use random_base2 to get exactly 2**m points
|
|
171
|
+
u = engine.random_base2(m) # shape: (n_total, dim)
|
|
172
|
+
# Guard against 0/1 values that map to +/-inf under ndtri
|
|
173
|
+
eps = 1e-12
|
|
174
|
+
u = np.clip(u, eps, 1.0 - eps)
|
|
175
|
+
|
|
176
|
+
# Transform uniforms to standard normals using ndtri
|
|
177
|
+
if special is None:
|
|
178
|
+
# Fallback, should not happen if _check_scipy passed
|
|
179
|
+
from scipy.stats import norm # type: ignore
|
|
180
|
+
|
|
181
|
+
z = norm.ppf(u)
|
|
182
|
+
else:
|
|
183
|
+
z = special.ndtri(u)
|
|
184
|
+
|
|
185
|
+
if n_paths != n_total:
|
|
186
|
+
z = z[:n_paths]
|
|
187
|
+
|
|
188
|
+
return np.asarray(z, dtype=float)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
__all__ = [
|
|
192
|
+
"RandomStream",
|
|
193
|
+
"PseudoRandomNormalGenerator",
|
|
194
|
+
"SobolNormalGenerator",
|
|
195
|
+
]
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Created on Mon Nov 17 2025
|
|
3
|
+
|
|
4
|
+
@author: yaofuxin
|
|
5
|
+
@description: Variance reduction utilities (antithetic variates, control
|
|
6
|
+
variates, importance sampling) designed to work with both
|
|
7
|
+
classical MC and QMC without destroying Sobol structure.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Optional, Tuple
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class VarianceReductionConfig:
|
|
20
|
+
"""
|
|
21
|
+
Configuration for variance reduction techniques.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
antithetic : bool
|
|
26
|
+
Whether to use antithetic variates. This is intended for classical
|
|
27
|
+
MC (pseudorandom) and is disabled for QMC by default.
|
|
28
|
+
control_variate : bool
|
|
29
|
+
Whether to construct a GBM-based control variate.
|
|
30
|
+
importance_sampling : bool
|
|
31
|
+
Whether to apply a mean shift to the driving Brownian increments.
|
|
32
|
+
importance_shift : float
|
|
33
|
+
Shift parameter used in importance sampling. The same shift is applied
|
|
34
|
+
to all normal dimensions.
|
|
35
|
+
control_variate_mu : float, optional
|
|
36
|
+
Drift parameter for the GBM control variate.
|
|
37
|
+
control_variate_sigma : float, optional
|
|
38
|
+
Volatility for the GBM control variate.
|
|
39
|
+
control_variate_T : float, optional
|
|
40
|
+
Maturity of the GBM control variate.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
antithetic: bool = False
|
|
44
|
+
control_variate: bool = False
|
|
45
|
+
importance_sampling: bool = False
|
|
46
|
+
importance_shift: float = 0.0
|
|
47
|
+
control_variate_mu: Optional[float] = None
|
|
48
|
+
control_variate_sigma: Optional[float] = None
|
|
49
|
+
control_variate_T: Optional[float] = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_antithetic_pairs(z: np.ndarray) -> np.ndarray:
|
|
53
|
+
"""
|
|
54
|
+
Construct antithetic variates from a base matrix of standard normals.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
z : np.ndarray
|
|
59
|
+
Base standard normals of shape (n_base_paths, dim).
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
np.ndarray
|
|
64
|
+
Array of shape (2 * n_base_paths, dim) containing [z, -z].
|
|
65
|
+
"""
|
|
66
|
+
z = np.asarray(z, dtype=float)
|
|
67
|
+
return np.concatenate([z, -z], axis=0)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def apply_importance_sampling_shift(z: np.ndarray, shift: float) -> np.ndarray:
|
|
71
|
+
"""
|
|
72
|
+
Apply a constant mean shift to standard normal samples.
|
|
73
|
+
|
|
74
|
+
This function is safe for QMC usage because it preserves the low-discrepancy
|
|
75
|
+
structure of the underlying sequence while changing the sampling measure.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
z : np.ndarray
|
|
80
|
+
Standard normal samples of shape (n_paths, dim).
|
|
81
|
+
shift : float
|
|
82
|
+
Constant shift applied to all dimensions.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
np.ndarray
|
|
87
|
+
Shifted standard normals of the same shape as z.
|
|
88
|
+
"""
|
|
89
|
+
z = np.asarray(z, dtype=float)
|
|
90
|
+
if shift == 0.0:
|
|
91
|
+
return z
|
|
92
|
+
return z + shift
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def importance_sampling_weights(z_shifted: np.ndarray, shift: float) -> np.ndarray:
|
|
96
|
+
"""
|
|
97
|
+
Compute likelihood ratio weights for Gaussian importance sampling.
|
|
98
|
+
|
|
99
|
+
If Z' ~ N(shift, 1), the Radon–Nikodym derivative dP/dQ at Z' is
|
|
100
|
+
|
|
101
|
+
w = exp(shift * Z' - 0.5 * shift^2)
|
|
102
|
+
|
|
103
|
+
For a d-dimensional vector of independent components, the total weight is
|
|
104
|
+
|
|
105
|
+
w = exp(shift * sum(Z'_j) - 0.5 * d * shift^2).
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
z_shifted : np.ndarray
|
|
110
|
+
Shifted standard normal samples of shape (n_paths, dim), i.e., samples
|
|
111
|
+
drawn from N(shift, 1) along each dimension.
|
|
112
|
+
shift : float
|
|
113
|
+
Shift parameter used in the importance sampling scheme.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
np.ndarray
|
|
118
|
+
Weights of shape (n_paths,) to reweight payoffs back to the original
|
|
119
|
+
N(0, 1) measure.
|
|
120
|
+
"""
|
|
121
|
+
z_shifted = np.asarray(z_shifted, dtype=float)
|
|
122
|
+
if z_shifted.ndim != 2:
|
|
123
|
+
raise ValueError("z_shifted must be a 2D array of shape (n_paths, dim)")
|
|
124
|
+
if shift == 0.0:
|
|
125
|
+
return np.ones(z_shifted.shape[0], dtype=float)
|
|
126
|
+
|
|
127
|
+
n_paths, dim = z_shifted.shape
|
|
128
|
+
sum_z = np.sum(z_shifted, axis=1)
|
|
129
|
+
exponent = shift * sum_z - 0.5 * (shift ** 2) * dim
|
|
130
|
+
return np.exp(exponent)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def gbm_control_variate(
|
|
134
|
+
z: np.ndarray,
|
|
135
|
+
mu: float,
|
|
136
|
+
sigma: float,
|
|
137
|
+
T: float,
|
|
138
|
+
) -> np.ndarray:
|
|
139
|
+
"""
|
|
140
|
+
Construct a GBM-based control variate using the same driving normals.
|
|
141
|
+
|
|
142
|
+
This control variate follows the exact GBM solution:
|
|
143
|
+
|
|
144
|
+
S_t = exp((mu - 0.5 * sigma^2) * t + sigma * sqrt(t) * Z)
|
|
145
|
+
|
|
146
|
+
using the column-wise time points.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
z : np.ndarray
|
|
151
|
+
Standard normal samples of shape (n_paths, n_steps).
|
|
152
|
+
mu : float
|
|
153
|
+
Drift parameter.
|
|
154
|
+
sigma : float
|
|
155
|
+
Volatility parameter.
|
|
156
|
+
T : float
|
|
157
|
+
Maturity.
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
np.ndarray
|
|
162
|
+
Control variate array of shape (n_paths, n_steps), with one control
|
|
163
|
+
variate value per time step along each path.
|
|
164
|
+
"""
|
|
165
|
+
z = np.asarray(z, dtype=float)
|
|
166
|
+
if z.ndim != 2:
|
|
167
|
+
raise ValueError("z must be a 2D array of shape (n_paths, n_steps)")
|
|
168
|
+
|
|
169
|
+
n_steps = z.shape[1]
|
|
170
|
+
if n_steps <= 0:
|
|
171
|
+
raise ValueError("n_steps must be positive")
|
|
172
|
+
|
|
173
|
+
dt = T / n_steps
|
|
174
|
+
t = np.arange(1, n_steps + 1, dtype=float) * dt
|
|
175
|
+
|
|
176
|
+
drift = (mu - 0.5 * sigma * sigma) * t
|
|
177
|
+
diffusion = sigma * np.sqrt(t)
|
|
178
|
+
|
|
179
|
+
drift = drift.reshape(1, -1)
|
|
180
|
+
diffusion = diffusion.reshape(1, -1)
|
|
181
|
+
|
|
182
|
+
return np.exp(drift + diffusion * z)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def apply_variance_reduction_to_normals(
|
|
186
|
+
n_paths: int,
|
|
187
|
+
dim: int,
|
|
188
|
+
base_normals: np.ndarray,
|
|
189
|
+
vr_config: Optional[VarianceReductionConfig],
|
|
190
|
+
is_qmc: bool,
|
|
191
|
+
) -> Tuple[np.ndarray, Optional[np.ndarray], Optional[np.ndarray]]:
|
|
192
|
+
"""
|
|
193
|
+
Apply configured variance reduction techniques to a matrix of normals.
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
n_paths : int
|
|
198
|
+
Desired final number of paths.
|
|
199
|
+
dim : int
|
|
200
|
+
Dimension of each path.
|
|
201
|
+
base_normals : np.ndarray
|
|
202
|
+
Base N(0, 1) samples of shape (n_paths, dim) or smaller if antithetic
|
|
203
|
+
variates will expand the path count.
|
|
204
|
+
vr_config : VarianceReductionConfig, optional
|
|
205
|
+
Variance reduction configuration.
|
|
206
|
+
is_qmc : bool
|
|
207
|
+
Whether the underlying sampling is QMC (Sobol). Antithetic variates are
|
|
208
|
+
disabled by default in QMC mode to avoid destroying low-discrepancy
|
|
209
|
+
structure.
|
|
210
|
+
|
|
211
|
+
Returns
|
|
212
|
+
-------
|
|
213
|
+
z_processed : np.ndarray
|
|
214
|
+
Processed normals of shape (n_paths, dim) after applying antithetic
|
|
215
|
+
variates and/or importance sampling.
|
|
216
|
+
weights : np.ndarray or None
|
|
217
|
+
Importance sampling weights if enabled, otherwise None.
|
|
218
|
+
control_variate : np.ndarray or None
|
|
219
|
+
GBM-based control variate if enabled, otherwise None.
|
|
220
|
+
"""
|
|
221
|
+
z = np.asarray(base_normals, dtype=float)
|
|
222
|
+
if z.ndim != 2:
|
|
223
|
+
raise ValueError("base_normals must be a 2D array")
|
|
224
|
+
|
|
225
|
+
weights: Optional[np.ndarray] = None
|
|
226
|
+
control_variate: Optional[np.ndarray] = None
|
|
227
|
+
|
|
228
|
+
if vr_config is None:
|
|
229
|
+
if z.shape != (n_paths, dim):
|
|
230
|
+
raise ValueError(
|
|
231
|
+
f"Expected base_normals to have shape ({n_paths}, {dim}) without "
|
|
232
|
+
f"variance reduction, got {z.shape}."
|
|
233
|
+
)
|
|
234
|
+
return z, weights, control_variate
|
|
235
|
+
|
|
236
|
+
# Antithetic variates: only for classical MC by default.
|
|
237
|
+
if vr_config.antithetic:
|
|
238
|
+
if is_qmc:
|
|
239
|
+
raise ValueError(
|
|
240
|
+
"Antithetic variates are not enabled for QMC mode to avoid "
|
|
241
|
+
"breaking Sobol low-discrepancy properties."
|
|
242
|
+
)
|
|
243
|
+
# base_normals is assumed to have shape (ceil(n_paths / 2), dim)
|
|
244
|
+
z = build_antithetic_pairs(z)
|
|
245
|
+
if z.shape[0] < n_paths:
|
|
246
|
+
raise ValueError(
|
|
247
|
+
"Antithetic base_normals must be large enough to reach n_paths."
|
|
248
|
+
)
|
|
249
|
+
z = z[:n_paths, :]
|
|
250
|
+
|
|
251
|
+
# Importance sampling (safe for both MC and QMC)
|
|
252
|
+
if vr_config.importance_sampling and vr_config.importance_shift != 0.0:
|
|
253
|
+
z = apply_importance_sampling_shift(z, vr_config.importance_shift)
|
|
254
|
+
weights = importance_sampling_weights(z, vr_config.importance_shift)
|
|
255
|
+
|
|
256
|
+
# Control variate based on GBM analytical solution
|
|
257
|
+
if vr_config.control_variate:
|
|
258
|
+
if (
|
|
259
|
+
vr_config.control_variate_mu is None
|
|
260
|
+
or vr_config.control_variate_sigma is None
|
|
261
|
+
or vr_config.control_variate_T is None
|
|
262
|
+
):
|
|
263
|
+
raise ValueError(
|
|
264
|
+
"control_variate_mu, control_variate_sigma and control_variate_T "
|
|
265
|
+
"must be provided in VarianceReductionConfig when control_variate "
|
|
266
|
+
"is enabled."
|
|
267
|
+
)
|
|
268
|
+
control_variate = gbm_control_variate(
|
|
269
|
+
z,
|
|
270
|
+
mu=vr_config.control_variate_mu,
|
|
271
|
+
sigma=vr_config.control_variate_sigma,
|
|
272
|
+
T=vr_config.control_variate_T,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
if z.shape != (n_paths, dim):
|
|
276
|
+
raise ValueError(
|
|
277
|
+
f"Processed normals have shape {z.shape}, expected ({n_paths}, {dim})."
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
return z, weights, control_variate
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
__all__ = [
|
|
284
|
+
"VarianceReductionConfig",
|
|
285
|
+
"build_antithetic_pairs",
|
|
286
|
+
"apply_importance_sampling_shift",
|
|
287
|
+
"importance_sampling_weights",
|
|
288
|
+
"gbm_control_variate",
|
|
289
|
+
"apply_variance_reduction_to_normals",
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
|